Skip to content

Commit 63743d5

Browse files
committed
Merge pull request #773 from stefanbirkner/parameterized-runner-factory
Parameterized runner factory
2 parents c399dd6 + cb6abf6 commit 63743d5

File tree

10 files changed

+628
-150
lines changed

10 files changed

+628
-150
lines changed

src/main/java/org/junit/runners/Parameterized.java

Lines changed: 103 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
package org.junit.runners;
22

3-
import java.lang.annotation.Annotation;
43
import java.lang.annotation.ElementType;
54
import java.lang.annotation.Retention;
65
import java.lang.annotation.RetentionPolicy;
76
import java.lang.annotation.Target;
8-
import java.lang.reflect.Field;
97
import java.text.MessageFormat;
108
import java.util.ArrayList;
119
import java.util.Arrays;
1210
import java.util.Collections;
1311
import java.util.List;
1412

1513
import org.junit.runner.Runner;
16-
import org.junit.runner.notification.RunNotifier;
17-
import org.junit.runners.model.FrameworkField;
1814
import org.junit.runners.model.FrameworkMethod;
1915
import org.junit.runners.model.InitializationError;
20-
import org.junit.runners.model.Statement;
16+
import org.junit.runners.model.TestClass;
17+
import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParametersFactory;
18+
import org.junit.runners.parameterized.ParametersRunnerFactory;
19+
import org.junit.runners.parameterized.TestWithParameters;
2120

2221
/**
2322
* The custom runner <code>Parameterized</code> implements parameterized tests.
@@ -130,6 +129,36 @@
130129
* }
131130
* </pre>
132131
*
132+
* <h3>Create different runners</h3>
133+
* <p>
134+
* By default the {@code Parameterized} runner creates a slightly modified
135+
* {@link BlockJUnit4ClassRunner} for each set of parameters. You can build an
136+
* own {@code Parameterized} runner that creates another runner for each set of
137+
* parameters. Therefore you have to build a {@link ParametersRunnerFactory}
138+
* that creates a runner for each {@link TestWithParameters}. (
139+
* {@code TestWithParameters} are bundling the parameters and the test name.)
140+
* The factory must have a public zero-arg constructor.
141+
*
142+
* <pre>
143+
* public class YourRunnerFactory implements ParameterizedRunnerFactory {
144+
* public Runner createRunnerForTestWithParameters(TestWithParameters test)
145+
* throws InitializationError {
146+
* return YourRunner(test);
147+
* }
148+
* }
149+
* </pre>
150+
* <p>
151+
* Use the {@link UseParametersRunnerFactory} to tell the {@code Parameterized}
152+
* runner that it should use your factory.
153+
*
154+
* <pre>
155+
* &#064;RunWith(Parameterized.class)
156+
* &#064;UseParametersRunnerFactory(YourRunnerFactory.class)
157+
* public class YourTest {
158+
* ...
159+
* }
160+
* </pre>
161+
*
133162
* @since 4.0
134163
*/
135164
public class Parameterized extends Suite {
@@ -183,118 +212,24 @@ public class Parameterized extends Suite {
183212
int value() default 0;
184213
}
185214

186-
protected class TestClassRunnerForParameters extends BlockJUnit4ClassRunner {
187-
private final Object[] fParameters;
188-
189-
private final String fName;
190-
191-
protected TestClassRunnerForParameters(Class<?> type, String pattern, int index, Object[] parameters) throws InitializationError {
192-
super(type);
193-
194-
fParameters = parameters;
195-
fName = nameFor(pattern, index, parameters);
196-
}
197-
198-
@Override
199-
public Object createTest() throws Exception {
200-
if (fieldsAreAnnotated()) {
201-
return createTestUsingFieldInjection();
202-
} else {
203-
return createTestUsingConstructorInjection();
204-
}
205-
}
206-
207-
private Object createTestUsingConstructorInjection() throws Exception {
208-
return getTestClass().getOnlyConstructor().newInstance(fParameters);
209-
}
210-
211-
private Object createTestUsingFieldInjection() throws Exception {
212-
List<FrameworkField> annotatedFieldsByParameter = getAnnotatedFieldsByParameter();
213-
if (annotatedFieldsByParameter.size() != fParameters.length) {
214-
throw new Exception("Wrong number of parameters and @Parameter fields." +
215-
" @Parameter fields counted: " + annotatedFieldsByParameter.size() + ", available parameters: " + fParameters.length + ".");
216-
}
217-
Object testClassInstance = getTestClass().getJavaClass().newInstance();
218-
for (FrameworkField each : annotatedFieldsByParameter) {
219-
Field field = each.getField();
220-
Parameter annotation = field.getAnnotation(Parameter.class);
221-
int index = annotation.value();
222-
try {
223-
field.set(testClassInstance, fParameters[index]);
224-
} catch (IllegalArgumentException iare) {
225-
throw new Exception(getTestClass().getName() + ": Trying to set " + field.getName() +
226-
" with the value " + fParameters[index] +
227-
" that is not the right type (" + fParameters[index].getClass().getSimpleName() + " instead of " +
228-
field.getType().getSimpleName() + ").", iare);
229-
}
230-
}
231-
return testClassInstance;
232-
}
233-
234-
protected String nameFor(String pattern, int index, Object[] parameters) {
235-
String finalPattern = pattern.replaceAll("\\{index\\}", Integer.toString(index));
236-
String name = MessageFormat.format(finalPattern, parameters);
237-
return "[" + name + "]";
238-
}
239-
240-
@Override
241-
protected String getName() {
242-
return fName;
243-
}
244-
245-
@Override
246-
protected String testName(FrameworkMethod method) {
247-
return method.getName() + getName();
248-
}
249-
250-
@Override
251-
protected void validateConstructor(List<Throwable> errors) {
252-
validateOnlyOneConstructor(errors);
253-
if (fieldsAreAnnotated()) {
254-
validateZeroArgConstructor(errors);
255-
}
256-
}
257-
258-
@Override
259-
protected void validateFields(List<Throwable> errors) {
260-
super.validateFields(errors);
261-
if (fieldsAreAnnotated()) {
262-
List<FrameworkField> annotatedFieldsByParameter = getAnnotatedFieldsByParameter();
263-
int[] usedIndices = new int[annotatedFieldsByParameter.size()];
264-
for (FrameworkField each : annotatedFieldsByParameter) {
265-
int index = each.getField().getAnnotation(Parameter.class).value();
266-
if (index < 0 || index > annotatedFieldsByParameter.size() - 1) {
267-
errors.add(
268-
new Exception("Invalid @Parameter value: " + index + ". @Parameter fields counted: " +
269-
annotatedFieldsByParameter.size() + ". Please use an index between 0 and " +
270-
(annotatedFieldsByParameter.size() - 1) + ".")
271-
);
272-
} else {
273-
usedIndices[index]++;
274-
}
275-
}
276-
for (int index = 0; index < usedIndices.length; index++) {
277-
int numberOfUse = usedIndices[index];
278-
if (numberOfUse == 0) {
279-
errors.add(new Exception("@Parameter(" + index + ") is never used."));
280-
} else if (numberOfUse > 1) {
281-
errors.add(new Exception("@Parameter(" + index + ") is used more than once (" + numberOfUse + ")."));
282-
}
283-
}
284-
}
285-
}
286-
287-
@Override
288-
protected Statement classBlock(RunNotifier notifier) {
289-
return childrenInvoker(notifier);
290-
}
291-
292-
@Override
293-
protected Annotation[] getRunnerAnnotations() {
294-
return new Annotation[0];
295-
}
215+
/**
216+
* Add this annotation to your test class if you want to generate a special
217+
* runner. You have to specify a {@link ParametersRunnerFactory} class that
218+
* creates such runners. The factory must have a public zero-arg
219+
* constructor.
220+
*/
221+
@Retention(RetentionPolicy.RUNTIME)
222+
@Target(ElementType.TYPE)
223+
public @interface UseParametersRunnerFactory {
224+
/**
225+
* @return a {@link ParametersRunnerFactory} class (must have a default
226+
* constructor)
227+
*/
228+
Class<? extends ParametersRunnerFactory> value() default BlockJUnit4ClassRunnerWithParametersFactory.class;
296229
}
297230

231+
private static final ParametersRunnerFactory DEFAULT_FACTORY = new BlockJUnit4ClassRunnerWithParametersFactory();
232+
298233
private static final List<Runner> NO_RUNNERS = Collections.<Runner>emptyList();
299234

300235
private final List<Runner> fRunners;
@@ -304,26 +239,38 @@ protected Annotation[] getRunnerAnnotations() {
304239
*/
305240
public Parameterized(Class<?> klass) throws Throwable {
306241
super(klass, NO_RUNNERS);
242+
ParametersRunnerFactory runnerFactory = getParametersRunnerFactory(
243+
klass);
307244
Parameters parameters = getParametersMethod().getAnnotation(
308245
Parameters.class);
309-
fRunners = Collections.unmodifiableList(createRunnersForParameters(allParameters(), parameters.name()));
246+
fRunners = Collections.unmodifiableList(createRunnersForParameters(
247+
allParameters(), parameters.name(), runnerFactory));
248+
}
249+
250+
private ParametersRunnerFactory getParametersRunnerFactory(Class<?> klass)
251+
throws InstantiationException, IllegalAccessException {
252+
UseParametersRunnerFactory annotation = klass
253+
.getAnnotation(UseParametersRunnerFactory.class);
254+
if (annotation == null) {
255+
return DEFAULT_FACTORY;
256+
} else {
257+
Class<? extends ParametersRunnerFactory> factoryClass = annotation
258+
.value();
259+
return factoryClass.newInstance();
260+
}
310261
}
311262

312263
@Override
313264
protected List<Runner> getChildren() {
314265
return fRunners;
315266
}
316267

317-
private Runner createRunnerWithNotNormalizedParameters(String pattern,
318-
int index, Object parametersOrSingleParameter)
319-
throws InitializationError {
268+
private TestWithParameters createTestWithNotNormalizedParameters(
269+
String pattern, int index, Object parametersOrSingleParameter) {
320270
Object[] parameters= (parametersOrSingleParameter instanceof Object[]) ? (Object[]) parametersOrSingleParameter
321271
: new Object[] { parametersOrSingleParameter };
322-
return createRunner(pattern, index, parameters);
323-
}
324-
325-
protected Runner createRunner(String pattern, int index, Object[] parameters) throws InitializationError {
326-
return new TestClassRunnerForParameters(getTestClass().getJavaClass(), pattern, index, parameters);
272+
return createTestWithParameters(getTestClass(), pattern, index,
273+
parameters);
327274
}
328275

329276
@SuppressWarnings("unchecked")
@@ -351,20 +298,37 @@ private FrameworkMethod getParametersMethod() throws Exception {
351298
+ getTestClass().getName());
352299
}
353300

354-
private List<Runner> createRunnersForParameters(Iterable<Object> allParameters, String namePattern) throws Exception {
301+
private List<Runner> createRunnersForParameters(
302+
Iterable<Object> allParameters, String namePattern,
303+
ParametersRunnerFactory runnerFactory)
304+
throws InitializationError,
305+
Exception {
355306
try {
356-
int i = 0;
357-
List<Runner> children = new ArrayList<Runner>();
358-
for (Object parametersOfSingleTest : allParameters) {
359-
children.add(createRunnerWithNotNormalizedParameters(
360-
namePattern, i++, parametersOfSingleTest));
307+
List<TestWithParameters> tests = createTestsForParameters(
308+
allParameters, namePattern);
309+
List<Runner> runners = new ArrayList<Runner>();
310+
for (TestWithParameters test : tests) {
311+
runners.add(runnerFactory
312+
.createRunnerForTestWithParameters(test));
361313
}
362-
return children;
314+
return runners;
363315
} catch (ClassCastException e) {
364316
throw parametersMethodReturnedWrongType();
365317
}
366318
}
367319

320+
private List<TestWithParameters> createTestsForParameters(
321+
Iterable<Object> allParameters, String namePattern)
322+
throws Exception {
323+
int i = 0;
324+
List<TestWithParameters> children = new ArrayList<TestWithParameters>();
325+
for (Object parametersOfSingleTest : allParameters) {
326+
children.add(createTestWithNotNormalizedParameters(namePattern,
327+
i++, parametersOfSingleTest));
328+
}
329+
return children;
330+
}
331+
368332
private Exception parametersMethodReturnedWrongType() throws Exception {
369333
String className = getTestClass().getName();
370334
String methodName = getParametersMethod().getName();
@@ -374,11 +338,12 @@ private Exception parametersMethodReturnedWrongType() throws Exception {
374338
return new Exception(message);
375339
}
376340

377-
private List<FrameworkField> getAnnotatedFieldsByParameter() {
378-
return getTestClass().getAnnotatedFields(Parameter.class);
379-
}
380-
381-
private boolean fieldsAreAnnotated() {
382-
return !getAnnotatedFieldsByParameter().isEmpty();
341+
private static TestWithParameters createTestWithParameters(
342+
TestClass testClass, String pattern, int index, Object[] parameters) {
343+
String finalPattern = pattern.replaceAll("\\{index\\}",
344+
Integer.toString(index));
345+
String name = MessageFormat.format(finalPattern, parameters);
346+
return new TestWithParameters("[" + name + "]", testClass,
347+
Arrays.asList(parameters));
383348
}
384-
}
349+
}

src/main/java/org/junit/runners/model/TestClass.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,26 @@ public boolean isANonStaticInnerClass() {
252252
return fClass.isMemberClass() && !isStatic(fClass.getModifiers());
253253
}
254254

255+
@Override
256+
public int hashCode() {
257+
return (fClass == null) ? 0 : fClass.hashCode();
258+
}
259+
260+
@Override
261+
public boolean equals(Object obj) {
262+
if (this == obj) {
263+
return true;
264+
}
265+
if (obj == null) {
266+
return false;
267+
}
268+
if (getClass() != obj.getClass()) {
269+
return false;
270+
}
271+
TestClass other = (TestClass) obj;
272+
return fClass == other.fClass;
273+
}
274+
255275
/**
256276
* Compares two fields by its name.
257277
*/

0 commit comments

Comments
 (0)