From 41092ce4e01cc57d8aa1911e2f1169e0f355b1ae Mon Sep 17 00:00:00 2001 From: Stefan Birkner Date: Thu, 31 Oct 2013 23:54:25 +0100 Subject: [PATCH 1/4] Move TestClassTest to package org.junit.runners.model. It is common to add tests to the same package as the class under test. --- .../running/classes => runners/model}/TestClassTest.java | 5 +---- src/test/java/org/junit/tests/AllTests.java | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) rename src/test/java/org/junit/{tests/running/classes => runners/model}/TestClassTest.java (95%) diff --git a/src/test/java/org/junit/tests/running/classes/TestClassTest.java b/src/test/java/org/junit/runners/model/TestClassTest.java similarity index 95% rename from src/test/java/org/junit/tests/running/classes/TestClassTest.java rename to src/test/java/org/junit/runners/model/TestClassTest.java index 4a584d01468d..5331fd89b261 100644 --- a/src/test/java/org/junit/tests/running/classes/TestClassTest.java +++ b/src/test/java/org/junit/runners/model/TestClassTest.java @@ -1,4 +1,4 @@ -package org.junit.tests.running.classes; +package org.junit.runners.model; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; @@ -11,9 +11,6 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TestRule; -import org.junit.runners.model.FrameworkField; -import org.junit.runners.model.FrameworkMethod; -import org.junit.runners.model.TestClass; public class TestClassTest { diff --git a/src/test/java/org/junit/tests/AllTests.java b/src/test/java/org/junit/tests/AllTests.java index ad356a737454..715ba86f912b 100644 --- a/src/test/java/org/junit/tests/AllTests.java +++ b/src/test/java/org/junit/tests/AllTests.java @@ -17,6 +17,7 @@ import org.junit.runners.Suite.SuiteClasses; import org.junit.runners.model.FrameworkFieldTest; import org.junit.runners.model.FrameworkMethodTest; +import org.junit.runners.model.TestClassTest; import org.junit.tests.assertion.AssertionTest; import org.junit.tests.assertion.ComparisonFailureTest; import org.junit.tests.assertion.MultipleFailureExceptionTest; @@ -85,7 +86,6 @@ import org.junit.tests.running.classes.ParentRunnerTest; import org.junit.tests.running.classes.RunWithTest; import org.junit.tests.running.classes.SuiteTest; -import org.junit.tests.running.classes.TestClassTest; import org.junit.tests.running.classes.UseSuiteAsASuperclassTest; import org.junit.tests.running.core.CommandLineTest; import org.junit.tests.running.core.JUnitCoreReturnsCorrectExitCodeTest; From 3e08f69912d69189e895c279b1bece0c7c0840f4 Mon Sep 17 00:00:00 2001 From: Stefan Birkner Date: Thu, 21 Nov 2013 08:45:04 +0100 Subject: [PATCH 2/4] Create a class TestWithParameters. This class keeps the data together that are needed for creating a runner for a single data set of a parameterized test. This makes it also clear, that the computation of the name is not the responsibility of the runner but of the Parameterized class. --- .../java/org/junit/runners/Parameterized.java | 44 ++++-- .../org/junit/runners/model/TestClass.java | 20 +++ .../parameterized/TestWithParameters.java | 82 +++++++++++ .../junit/runners/model/TestClassTest.java | 54 +++++++ .../parameterized/TestWithParametersTest.java | 137 ++++++++++++++++++ src/test/java/org/junit/tests/AllTests.java | 4 +- 6 files changed, 327 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/junit/runners/parameterized/TestWithParameters.java create mode 100644 src/test/java/org/junit/runners/parameterized/TestWithParametersTest.java diff --git a/src/main/java/org/junit/runners/Parameterized.java b/src/main/java/org/junit/runners/Parameterized.java index 9b1cc0200210..856f1a817a5f 100644 --- a/src/main/java/org/junit/runners/Parameterized.java +++ b/src/main/java/org/junit/runners/Parameterized.java @@ -18,6 +18,8 @@ import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; +import org.junit.runners.parameterized.TestWithParameters; /** * The custom runner Parameterized implements parameterized tests. @@ -188,11 +190,21 @@ protected class TestClassRunnerForParameters extends BlockJUnit4ClassRunner { private final String fName; + /** + * @deprecated please use + * {@code TestClassRunnerForParameters(TestWithParameters)} + */ + @Deprecated protected TestClassRunnerForParameters(Class type, String pattern, int index, Object[] parameters) throws InitializationError { - super(type); + this(createTestWithParameters(new TestClass(type), pattern, index, + parameters)); + } - fParameters = parameters; - fName = nameFor(pattern, index, parameters); + protected TestClassRunnerForParameters(TestWithParameters test) + throws InitializationError { + super(test.getTestClass().getJavaClass()); + fParameters = test.getParameters().toArray(); + fName = test.getName(); } @Override @@ -231,12 +243,6 @@ private Object createTestUsingFieldInjection() throws Exception { return testClassInstance; } - protected String nameFor(String pattern, int index, Object[] parameters) { - String finalPattern = pattern.replaceAll("\\{index\\}", Integer.toString(index)); - String name = MessageFormat.format(finalPattern, parameters); - return "[" + name + "]"; - } - @Override protected String getName() { return fName; @@ -319,11 +325,14 @@ private Runner createRunnerWithNotNormalizedParameters(String pattern, throws InitializationError { Object[] parameters= (parametersOrSingleParameter instanceof Object[]) ? (Object[]) parametersOrSingleParameter : new Object[] { parametersOrSingleParameter }; - return createRunner(pattern, index, parameters); + TestWithParameters test = createTestWithParameters(getTestClass(), + pattern, index, parameters); + return createRunnerForTest(test); } - protected Runner createRunner(String pattern, int index, Object[] parameters) throws InitializationError { - return new TestClassRunnerForParameters(getTestClass().getJavaClass(), pattern, index, parameters); + protected Runner createRunnerForTest(TestWithParameters test) + throws InitializationError { + return new TestClassRunnerForParameters(test); } @SuppressWarnings("unchecked") @@ -381,4 +390,13 @@ private List getAnnotatedFieldsByParameter() { private boolean fieldsAreAnnotated() { return !getAnnotatedFieldsByParameter().isEmpty(); } -} \ No newline at end of file + + private static TestWithParameters createTestWithParameters( + TestClass testClass, String pattern, int index, Object[] parameters) { + String finalPattern = pattern.replaceAll("\\{index\\}", + Integer.toString(index)); + String name = MessageFormat.format(finalPattern, parameters); + return new TestWithParameters("[" + name + "]", testClass, + Arrays.asList(parameters)); + } +} diff --git a/src/main/java/org/junit/runners/model/TestClass.java b/src/main/java/org/junit/runners/model/TestClass.java index 6a265757eb86..e02ec9bb87db 100755 --- a/src/main/java/org/junit/runners/model/TestClass.java +++ b/src/main/java/org/junit/runners/model/TestClass.java @@ -252,6 +252,26 @@ public boolean isANonStaticInnerClass() { return fClass.isMemberClass() && !isStatic(fClass.getModifiers()); } + @Override + public int hashCode() { + return (fClass == null) ? 0 : fClass.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TestClass other = (TestClass) obj; + return fClass == other.fClass; + } + /** * Compares two fields by its name. */ diff --git a/src/main/java/org/junit/runners/parameterized/TestWithParameters.java b/src/main/java/org/junit/runners/parameterized/TestWithParameters.java new file mode 100644 index 000000000000..52dae5fa2557 --- /dev/null +++ b/src/main/java/org/junit/runners/parameterized/TestWithParameters.java @@ -0,0 +1,82 @@ +package org.junit.runners.parameterized; + +import static java.util.Collections.unmodifiableList; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.runners.model.TestClass; + +/** + * A {@code TestWithParameters} keeps the data together that are needed for + * creating a runner for a single data set of a parameterized test. It has a + * name, the test class and a list of parameters. + * + * @since 4.12 + */ +public class TestWithParameters { + private final String fName; + + private final TestClass fTestClass; + + private final List fParameters; + + public TestWithParameters(String name, TestClass testClass, + List parameters) { + notNull(name, "The name is missing."); + notNull(testClass, "The test class is missing."); + notNull(parameters, "The parameters are missing."); + fName = name; + fTestClass = testClass; + fParameters = unmodifiableList(new ArrayList(parameters)); + } + + public String getName() { + return fName; + } + + public TestClass getTestClass() { + return fTestClass; + } + + public List getParameters() { + return fParameters; + } + + @Override + public int hashCode() { + int prime = 14747; + int result = prime + fName.hashCode(); + result = prime * result + fTestClass.hashCode(); + return prime * result + fParameters.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TestWithParameters other = (TestWithParameters) obj; + return fName.equals(other.fName) + && fParameters.equals(other.fParameters) + && fTestClass.equals(other.fTestClass); + } + + @Override + public String toString() { + return fTestClass.getName() + " '" + fName + "' with parameters " + + fParameters; + } + + private static void notNull(Object value, String message) { + if (value == null) { + throw new NullPointerException(message); + } + } +} diff --git a/src/test/java/org/junit/runners/model/TestClassTest.java b/src/test/java/org/junit/runners/model/TestClassTest.java index 5331fd89b261..beb7ecebbf3d 100644 --- a/src/test/java/org/junit/runners/model/TestClassTest.java +++ b/src/test/java/org/junit/runners/model/TestClassTest.java @@ -2,7 +2,10 @@ import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import java.util.List; @@ -147,4 +150,55 @@ public void annotatedMethodValues() { assertThat(values, hasItem("jupiter")); assertThat(values.size(), is(1)); } + + @Test + public void isEqualToTestClassThatWrapsSameJavaClass() { + TestClass testClass = new TestClass(DummyClass.class); + TestClass testClassThatWrapsSameJavaClass = new TestClass( + DummyClass.class); + assertTrue(testClass.equals(testClassThatWrapsSameJavaClass)); + } + + @Test + public void isEqualToTestClassThatWrapsNoJavaClassToo() { + TestClass testClass = new TestClass(null); + TestClass testClassThatWrapsNoJavaClassToo = new TestClass(null); + assertTrue(testClass.equals(testClassThatWrapsNoJavaClassToo)); + } + + @Test + public void isNotEqualToTestClassThatWrapsADifferentJavaClass() { + TestClass testClass = new TestClass(DummyClass.class); + TestClass testClassThatWrapsADifferentJavaClass = new TestClass( + AnotherDummyClass.class); + assertFalse(testClass.equals(testClassThatWrapsADifferentJavaClass)); + } + + @Test + public void isNotEqualToNull() { + TestClass testClass = new TestClass(DummyClass.class); + assertFalse(testClass.equals(null)); + } + + private static class DummyClass { + } + + private static class AnotherDummyClass { + } + + @Test + public void hasSameHashCodeAsTestClassThatWrapsSameJavaClass() { + TestClass testClass = new TestClass(DummyClass.class); + TestClass testClassThatWrapsSameJavaClass = new TestClass( + DummyClass.class); + assertEquals(testClass.hashCode(), + testClassThatWrapsSameJavaClass.hashCode()); + } + + @Test + public void hasHashCodeWithoutJavaClass() { + TestClass testClass = new TestClass(null); + testClass.hashCode(); + // everything is fine if no exception is thrown. + } } diff --git a/src/test/java/org/junit/runners/parameterized/TestWithParametersTest.java b/src/test/java/org/junit/runners/parameterized/TestWithParametersTest.java new file mode 100644 index 000000000000..b8156e9fbd7b --- /dev/null +++ b/src/test/java/org/junit/runners/parameterized/TestWithParametersTest.java @@ -0,0 +1,137 @@ +package org.junit.runners.parameterized; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.rules.ExpectedException.none; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runners.model.TestClass; + +public class TestWithParametersTest { + private static final String DUMMY_NAME = "dummy name"; + + private static final TestClass DUMMY_TEST_CLASS = new TestClass( + DummyClass.class); + + private static final List DUMMY_PARAMETERS = Arrays + . asList("a", "b"); + + @Rule + public final ExpectedException thrown = none(); + + @Test + public void cannotBeCreatedWithoutAName() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("The name is missing."); + new TestWithParameters(null, DUMMY_TEST_CLASS, DUMMY_PARAMETERS); + } + + @Test + public void cannotBeCreatedWithoutTestClass() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("The test class is missing."); + new TestWithParameters(DUMMY_NAME, null, DUMMY_PARAMETERS); + } + + @Test + public void cannotBeCreatedWithoutParameters() { + thrown.expect(NullPointerException.class); + thrown.expectMessage("The parameters are missing."); + new TestWithParameters(DUMMY_NAME, DUMMY_TEST_CLASS, + (List) null); + } + + @Test + public void doesNotAllowToModifyProvidedParameters() { + TestWithParameters test = new TestWithParameters(DUMMY_NAME, + DUMMY_TEST_CLASS, DUMMY_PARAMETERS); + thrown.expect(UnsupportedOperationException.class); + test.getParameters().set(0, "another parameter"); + } + + @Test + public void doesNotConsiderParametersWhichChangedAfterTestInstantiation() { + List parameters = Arrays. asList("dummy parameter"); + TestWithParameters test = new TestWithParameters(DUMMY_NAME, + DUMMY_TEST_CLASS, parameters); + parameters.set(0, "another parameter"); + assertEquals(asList("dummy parameter"), test.getParameters()); + } + + @Test + public void isEqualToTestWithSameNameAndTestClassAndParameters() { + TestWithParameters firstTest = new TestWithParameters(DUMMY_NAME, + new TestClass(DummyClass.class), Arrays. asList("a", + "b")); + TestWithParameters secondTest = new TestWithParameters(DUMMY_NAME, + new TestClass(DummyClass.class), Arrays. asList("a", + "b")); + assertEquals(firstTest, secondTest); + } + + @Test + public void isNotEqualToTestWithDifferentName() { + TestWithParameters firstTest = new TestWithParameters("name", + DUMMY_TEST_CLASS, DUMMY_PARAMETERS); + TestWithParameters secondTest = new TestWithParameters("another name", + DUMMY_TEST_CLASS, DUMMY_PARAMETERS); + assertNotEquals(firstTest, secondTest); + } + + @Test + public void isNotEqualToTestWithDifferentTestClass() { + TestWithParameters firstTest = new TestWithParameters(DUMMY_NAME, + new TestClass(DummyClass.class), DUMMY_PARAMETERS); + TestWithParameters secondTest = new TestWithParameters(DUMMY_NAME, + new TestClass(AnotherDummyClass.class), DUMMY_PARAMETERS); + assertNotEquals(firstTest, secondTest); + } + + @Test + public void isNotEqualToTestWithDifferentParameters() { + TestWithParameters firstTest = new TestWithParameters(DUMMY_NAME, + DUMMY_TEST_CLASS, Arrays. asList("a")); + TestWithParameters secondTest = new TestWithParameters(DUMMY_NAME, + DUMMY_TEST_CLASS, Arrays. asList("b")); + assertNotEquals(firstTest, secondTest); + } + + @Test + public void isNotEqualToObjectWithDifferentClass() { + TestWithParameters test = new TestWithParameters(DUMMY_NAME, + DUMMY_TEST_CLASS, DUMMY_PARAMETERS); + assertNotEquals(test, new Integer(3)); + } + + @Test + public void hasSameHashCodeAsEqualTest() { + TestWithParameters firstTest = new TestWithParameters(DUMMY_NAME, + DUMMY_TEST_CLASS, DUMMY_PARAMETERS); + TestWithParameters secondTest = new TestWithParameters(DUMMY_NAME, + DUMMY_TEST_CLASS, DUMMY_PARAMETERS); + assertEquals(firstTest.hashCode(), secondTest.hashCode()); + } + + @Test + public void hasMeaningfulToString() { + TestWithParameters test = new TestWithParameters("name", new TestClass( + DummyClass.class), Arrays. asList("first parameter", + "second parameter")); + assertEquals( + "Wrong toString().", + "org.junit.runners.parameterized.TestWithParametersTest$DummyClass 'name' with parameters [first parameter, second parameter]", + test.toString()); + } + + private static class DummyClass { + } + + private static class AnotherDummyClass { + } +} diff --git a/src/test/java/org/junit/tests/AllTests.java b/src/test/java/org/junit/tests/AllTests.java index 715ba86f912b..beab4d215d0c 100644 --- a/src/test/java/org/junit/tests/AllTests.java +++ b/src/test/java/org/junit/tests/AllTests.java @@ -18,6 +18,7 @@ import org.junit.runners.model.FrameworkFieldTest; import org.junit.runners.model.FrameworkMethodTest; import org.junit.runners.model.TestClassTest; +import org.junit.runners.parameterized.TestWithParametersTest; import org.junit.tests.assertion.AssertionTest; import org.junit.tests.assertion.ComparisonFailureTest; import org.junit.tests.assertion.MultipleFailureExceptionTest; @@ -202,7 +203,8 @@ FrameworkFieldTest.class, FrameworkMethodTest.class, FailOnTimeoutTest.class, - JUnitCoreTest.class + JUnitCoreTest.class, + TestWithParametersTest.class }) public class AllTests { public static Test suite() { From dca4a949f93bb85cc8ee0eb8789979a18aa3ffc7 Mon Sep 17 00:00:00 2001 From: Stefan Birkner Date: Fri, 22 Nov 2013 09:40:06 +0100 Subject: [PATCH 3/4] Extract BlockJUnit4ClassRunnerWithParameters. Separate the class from the Parameterized runner. Makes it clear, that it is designed for reuse. --- .../java/org/junit/runners/Parameterized.java | 132 +--------------- .../BlockJUnit4ClassRunnerWithParameters.java | 143 ++++++++++++++++++ 2 files changed, 145 insertions(+), 130 deletions(-) create mode 100644 src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java diff --git a/src/main/java/org/junit/runners/Parameterized.java b/src/main/java/org/junit/runners/Parameterized.java index 856f1a817a5f..968a066554cb 100644 --- a/src/main/java/org/junit/runners/Parameterized.java +++ b/src/main/java/org/junit/runners/Parameterized.java @@ -1,11 +1,9 @@ package org.junit.runners; -import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.reflect.Field; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -13,12 +11,10 @@ import java.util.List; import org.junit.runner.Runner; -import org.junit.runner.notification.RunNotifier; -import org.junit.runners.model.FrameworkField; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; -import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; +import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters; import org.junit.runners.parameterized.TestWithParameters; /** @@ -185,122 +181,6 @@ public class Parameterized extends Suite { int value() default 0; } - protected class TestClassRunnerForParameters extends BlockJUnit4ClassRunner { - private final Object[] fParameters; - - private final String fName; - - /** - * @deprecated please use - * {@code TestClassRunnerForParameters(TestWithParameters)} - */ - @Deprecated - protected TestClassRunnerForParameters(Class type, String pattern, int index, Object[] parameters) throws InitializationError { - this(createTestWithParameters(new TestClass(type), pattern, index, - parameters)); - } - - protected TestClassRunnerForParameters(TestWithParameters test) - throws InitializationError { - super(test.getTestClass().getJavaClass()); - fParameters = test.getParameters().toArray(); - fName = test.getName(); - } - - @Override - public Object createTest() throws Exception { - if (fieldsAreAnnotated()) { - return createTestUsingFieldInjection(); - } else { - return createTestUsingConstructorInjection(); - } - } - - private Object createTestUsingConstructorInjection() throws Exception { - return getTestClass().getOnlyConstructor().newInstance(fParameters); - } - - private Object createTestUsingFieldInjection() throws Exception { - List annotatedFieldsByParameter = getAnnotatedFieldsByParameter(); - if (annotatedFieldsByParameter.size() != fParameters.length) { - throw new Exception("Wrong number of parameters and @Parameter fields." + - " @Parameter fields counted: " + annotatedFieldsByParameter.size() + ", available parameters: " + fParameters.length + "."); - } - Object testClassInstance = getTestClass().getJavaClass().newInstance(); - for (FrameworkField each : annotatedFieldsByParameter) { - Field field = each.getField(); - Parameter annotation = field.getAnnotation(Parameter.class); - int index = annotation.value(); - try { - field.set(testClassInstance, fParameters[index]); - } catch (IllegalArgumentException iare) { - throw new Exception(getTestClass().getName() + ": Trying to set " + field.getName() + - " with the value " + fParameters[index] + - " that is not the right type (" + fParameters[index].getClass().getSimpleName() + " instead of " + - field.getType().getSimpleName() + ").", iare); - } - } - return testClassInstance; - } - - @Override - protected String getName() { - return fName; - } - - @Override - protected String testName(FrameworkMethod method) { - return method.getName() + getName(); - } - - @Override - protected void validateConstructor(List errors) { - validateOnlyOneConstructor(errors); - if (fieldsAreAnnotated()) { - validateZeroArgConstructor(errors); - } - } - - @Override - protected void validateFields(List errors) { - super.validateFields(errors); - if (fieldsAreAnnotated()) { - List annotatedFieldsByParameter = getAnnotatedFieldsByParameter(); - int[] usedIndices = new int[annotatedFieldsByParameter.size()]; - for (FrameworkField each : annotatedFieldsByParameter) { - int index = each.getField().getAnnotation(Parameter.class).value(); - if (index < 0 || index > annotatedFieldsByParameter.size() - 1) { - errors.add( - new Exception("Invalid @Parameter value: " + index + ". @Parameter fields counted: " + - annotatedFieldsByParameter.size() + ". Please use an index between 0 and " + - (annotatedFieldsByParameter.size() - 1) + ".") - ); - } else { - usedIndices[index]++; - } - } - for (int index = 0; index < usedIndices.length; index++) { - int numberOfUse = usedIndices[index]; - if (numberOfUse == 0) { - errors.add(new Exception("@Parameter(" + index + ") is never used.")); - } else if (numberOfUse > 1) { - errors.add(new Exception("@Parameter(" + index + ") is used more than once (" + numberOfUse + ").")); - } - } - } - } - - @Override - protected Statement classBlock(RunNotifier notifier) { - return childrenInvoker(notifier); - } - - @Override - protected Annotation[] getRunnerAnnotations() { - return new Annotation[0]; - } - } - private static final List NO_RUNNERS = Collections.emptyList(); private final List fRunners; @@ -332,7 +212,7 @@ private Runner createRunnerWithNotNormalizedParameters(String pattern, protected Runner createRunnerForTest(TestWithParameters test) throws InitializationError { - return new TestClassRunnerForParameters(test); + return new BlockJUnit4ClassRunnerWithParameters(test); } @SuppressWarnings("unchecked") @@ -383,14 +263,6 @@ private Exception parametersMethodReturnedWrongType() throws Exception { return new Exception(message); } - private List getAnnotatedFieldsByParameter() { - return getTestClass().getAnnotatedFields(Parameter.class); - } - - private boolean fieldsAreAnnotated() { - return !getAnnotatedFieldsByParameter().isEmpty(); - } - private static TestWithParameters createTestWithParameters( TestClass testClass, String pattern, int index, Object[] parameters) { String finalPattern = pattern.replaceAll("\\{index\\}", diff --git a/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java b/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java new file mode 100644 index 000000000000..e1b47954d19a --- /dev/null +++ b/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java @@ -0,0 +1,143 @@ +package org.junit.runners.parameterized; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.List; + +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.model.FrameworkField; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; + +/** + * A {@link BlockJUnit4ClassRunner} with parameters support. Parameters can be + * injected via constructor or into annotated fields. + */ +public class BlockJUnit4ClassRunnerWithParameters extends + BlockJUnit4ClassRunner { + private final Object[] fParameters; + + private final String fName; + + public BlockJUnit4ClassRunnerWithParameters(TestWithParameters test) + throws InitializationError { + super(test.getTestClass().getJavaClass()); + fParameters = test.getParameters().toArray( + new Object[test.getParameters().size()]); + fName = test.getName(); + } + + @Override + public Object createTest() throws Exception { + if (fieldsAreAnnotated()) { + return createTestUsingFieldInjection(); + } else { + return createTestUsingConstructorInjection(); + } + } + + private Object createTestUsingConstructorInjection() throws Exception { + return getTestClass().getOnlyConstructor().newInstance(fParameters); + } + + private Object createTestUsingFieldInjection() throws Exception { + List annotatedFieldsByParameter = getAnnotatedFieldsByParameter(); + if (annotatedFieldsByParameter.size() != fParameters.length) { + throw new Exception( + "Wrong number of parameters and @Parameter fields." + + " @Parameter fields counted: " + + annotatedFieldsByParameter.size() + + ", available parameters: " + fParameters.length + + "."); + } + Object testClassInstance = getTestClass().getJavaClass().newInstance(); + for (FrameworkField each : annotatedFieldsByParameter) { + Field field = each.getField(); + Parameter annotation = field.getAnnotation(Parameter.class); + int index = annotation.value(); + try { + field.set(testClassInstance, fParameters[index]); + } catch (IllegalArgumentException iare) { + throw new Exception(getTestClass().getName() + + ": Trying to set " + field.getName() + + " with the value " + fParameters[index] + + " that is not the right type (" + + fParameters[index].getClass().getSimpleName() + + " instead of " + field.getType().getSimpleName() + + ").", iare); + } + } + return testClassInstance; + } + + @Override + protected String getName() { + return fName; + } + + @Override + protected String testName(FrameworkMethod method) { + return method.getName() + getName(); + } + + @Override + protected void validateConstructor(List errors) { + validateOnlyOneConstructor(errors); + if (fieldsAreAnnotated()) { + validateZeroArgConstructor(errors); + } + } + + @Override + protected void validateFields(List errors) { + super.validateFields(errors); + if (fieldsAreAnnotated()) { + List annotatedFieldsByParameter = getAnnotatedFieldsByParameter(); + int[] usedIndices = new int[annotatedFieldsByParameter.size()]; + for (FrameworkField each : annotatedFieldsByParameter) { + int index = each.getField().getAnnotation(Parameter.class) + .value(); + if (index < 0 || index > annotatedFieldsByParameter.size() - 1) { + errors.add(new Exception("Invalid @Parameter value: " + + index + ". @Parameter fields counted: " + + annotatedFieldsByParameter.size() + + ". Please use an index between 0 and " + + (annotatedFieldsByParameter.size() - 1) + ".")); + } else { + usedIndices[index]++; + } + } + for (int index = 0; index < usedIndices.length; index++) { + int numberOfUse = usedIndices[index]; + if (numberOfUse == 0) { + errors.add(new Exception("@Parameter(" + index + + ") is never used.")); + } else if (numberOfUse > 1) { + errors.add(new Exception("@Parameter(" + index + + ") is used more than once (" + numberOfUse + ").")); + } + } + } + } + + @Override + protected Statement classBlock(RunNotifier notifier) { + return childrenInvoker(notifier); + } + + @Override + protected Annotation[] getRunnerAnnotations() { + return new Annotation[0]; + } + + private List getAnnotatedFieldsByParameter() { + return getTestClass().getAnnotatedFields(Parameter.class); + } + + private boolean fieldsAreAnnotated() { + return !getAnnotatedFieldsByParameter().isEmpty(); + } +} From cb6abf6fc729f07c5d60b0756e91af444400b4b5 Mon Sep 17 00:00:00 2001 From: Stefan Birkner Date: Mon, 25 Nov 2013 22:37:12 +0100 Subject: [PATCH 4/4] Use a ParametersRunnerFactory for creating the test for a set of parameters. There's now an explicit component for creating the runner of a single set of parameters. IMHO this is a better extension point than overriding a method. The factory can be specified by the @UseParametersRunnerFactory annotation or you can subclass the Parameterized class and provide the factory to its constructor. --- .../java/org/junit/runners/Parameterized.java | 115 +++++++++++++++--- ...Unit4ClassRunnerWithParametersFactory.java | 18 +++ .../ParametersRunnerFactory.java | 21 ++++ .../classes/ParameterizedTestTest.java | 51 +++++++- 4 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParametersFactory.java create mode 100644 src/main/java/org/junit/runners/parameterized/ParametersRunnerFactory.java diff --git a/src/main/java/org/junit/runners/Parameterized.java b/src/main/java/org/junit/runners/Parameterized.java index 968a066554cb..2ad082c8d26d 100644 --- a/src/main/java/org/junit/runners/Parameterized.java +++ b/src/main/java/org/junit/runners/Parameterized.java @@ -14,7 +14,8 @@ import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.TestClass; -import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters; +import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParametersFactory; +import org.junit.runners.parameterized.ParametersRunnerFactory; import org.junit.runners.parameterized.TestWithParameters; /** @@ -128,6 +129,36 @@ * } * * + *

Create different runners

+ *

+ * By default the {@code Parameterized} runner creates a slightly modified + * {@link BlockJUnit4ClassRunner} for each set of parameters. You can build an + * own {@code Parameterized} runner that creates another runner for each set of + * parameters. Therefore you have to build a {@link ParametersRunnerFactory} + * that creates a runner for each {@link TestWithParameters}. ( + * {@code TestWithParameters} are bundling the parameters and the test name.) + * The factory must have a public zero-arg constructor. + * + *

+ * public class YourRunnerFactory implements ParameterizedRunnerFactory {
+ *     public Runner createRunnerForTestWithParameters(TestWithParameters test)
+ *             throws InitializationError {
+ *         return YourRunner(test);
+ *     }
+ * }
+ * 
+ *

+ * Use the {@link UseParametersRunnerFactory} to tell the {@code Parameterized} + * runner that it should use your factory. + * + *

+ * @RunWith(Parameterized.class)
+ * @UseParametersRunnerFactory(YourRunnerFactory.class)
+ * public class YourTest {
+ *     ...
+ * }
+ * 
+ * * @since 4.0 */ public class Parameterized extends Suite { @@ -181,6 +212,24 @@ public class Parameterized extends Suite { int value() default 0; } + /** + * Add this annotation to your test class if you want to generate a special + * runner. You have to specify a {@link ParametersRunnerFactory} class that + * creates such runners. The factory must have a public zero-arg + * constructor. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface UseParametersRunnerFactory { + /** + * @return a {@link ParametersRunnerFactory} class (must have a default + * constructor) + */ + Class value() default BlockJUnit4ClassRunnerWithParametersFactory.class; + } + + private static final ParametersRunnerFactory DEFAULT_FACTORY = new BlockJUnit4ClassRunnerWithParametersFactory(); + private static final List NO_RUNNERS = Collections.emptyList(); private final List fRunners; @@ -190,9 +239,25 @@ public class Parameterized extends Suite { */ public Parameterized(Class klass) throws Throwable { super(klass, NO_RUNNERS); + ParametersRunnerFactory runnerFactory = getParametersRunnerFactory( + klass); Parameters parameters = getParametersMethod().getAnnotation( Parameters.class); - fRunners = Collections.unmodifiableList(createRunnersForParameters(allParameters(), parameters.name())); + fRunners = Collections.unmodifiableList(createRunnersForParameters( + allParameters(), parameters.name(), runnerFactory)); + } + + private ParametersRunnerFactory getParametersRunnerFactory(Class klass) + throws InstantiationException, IllegalAccessException { + UseParametersRunnerFactory annotation = klass + .getAnnotation(UseParametersRunnerFactory.class); + if (annotation == null) { + return DEFAULT_FACTORY; + } else { + Class factoryClass = annotation + .value(); + return factoryClass.newInstance(); + } } @Override @@ -200,19 +265,12 @@ protected List getChildren() { return fRunners; } - private Runner createRunnerWithNotNormalizedParameters(String pattern, - int index, Object parametersOrSingleParameter) - throws InitializationError { + private TestWithParameters createTestWithNotNormalizedParameters( + String pattern, int index, Object parametersOrSingleParameter) { Object[] parameters= (parametersOrSingleParameter instanceof Object[]) ? (Object[]) parametersOrSingleParameter : new Object[] { parametersOrSingleParameter }; - TestWithParameters test = createTestWithParameters(getTestClass(), - pattern, index, parameters); - return createRunnerForTest(test); - } - - protected Runner createRunnerForTest(TestWithParameters test) - throws InitializationError { - return new BlockJUnit4ClassRunnerWithParameters(test); + return createTestWithParameters(getTestClass(), pattern, index, + parameters); } @SuppressWarnings("unchecked") @@ -240,20 +298,37 @@ private FrameworkMethod getParametersMethod() throws Exception { + getTestClass().getName()); } - private List createRunnersForParameters(Iterable allParameters, String namePattern) throws Exception { + private List createRunnersForParameters( + Iterable allParameters, String namePattern, + ParametersRunnerFactory runnerFactory) + throws InitializationError, + Exception { try { - int i = 0; - List children = new ArrayList(); - for (Object parametersOfSingleTest : allParameters) { - children.add(createRunnerWithNotNormalizedParameters( - namePattern, i++, parametersOfSingleTest)); + List tests = createTestsForParameters( + allParameters, namePattern); + List runners = new ArrayList(); + for (TestWithParameters test : tests) { + runners.add(runnerFactory + .createRunnerForTestWithParameters(test)); } - return children; + return runners; } catch (ClassCastException e) { throw parametersMethodReturnedWrongType(); } } + private List createTestsForParameters( + Iterable allParameters, String namePattern) + throws Exception { + int i = 0; + List children = new ArrayList(); + for (Object parametersOfSingleTest : allParameters) { + children.add(createTestWithNotNormalizedParameters(namePattern, + i++, parametersOfSingleTest)); + } + return children; + } + private Exception parametersMethodReturnedWrongType() throws Exception { String className = getTestClass().getName(); String methodName = getParametersMethod().getName(); diff --git a/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParametersFactory.java b/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParametersFactory.java new file mode 100644 index 000000000000..ae49ef4ba73b --- /dev/null +++ b/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParametersFactory.java @@ -0,0 +1,18 @@ +package org.junit.runners.parameterized; + +import org.junit.runner.Runner; +import org.junit.runners.model.InitializationError; + +/** + * A {@link ParametersRunnerFactory} that creates + * {@link BlockJUnit4ClassRunnerWithParameters}. + * + * @since 4.12 + */ +public class BlockJUnit4ClassRunnerWithParametersFactory implements + ParametersRunnerFactory { + public Runner createRunnerForTestWithParameters(TestWithParameters test) + throws InitializationError { + return new BlockJUnit4ClassRunnerWithParameters(test); + } +} diff --git a/src/main/java/org/junit/runners/parameterized/ParametersRunnerFactory.java b/src/main/java/org/junit/runners/parameterized/ParametersRunnerFactory.java new file mode 100644 index 000000000000..16ea1f30e029 --- /dev/null +++ b/src/main/java/org/junit/runners/parameterized/ParametersRunnerFactory.java @@ -0,0 +1,21 @@ +package org.junit.runners.parameterized; + +import org.junit.runner.Runner; +import org.junit.runners.model.InitializationError; + +/** + * A {@code ParameterizedRunnerFactory} creates a runner for a single + * {@link TestWithParameters}. + * + * @since 4.12 + */ +public interface ParametersRunnerFactory { + /** + * Returns a runner for the specified {@link TestWithParameters}. + * + * @throws InitializationError + * if the runner could not be created. + */ + Runner createRunnerForTestWithParameters(TestWithParameters test) + throws InitializationError; +} diff --git a/src/test/java/org/junit/tests/running/classes/ParameterizedTestTest.java b/src/test/java/org/junit/tests/running/classes/ParameterizedTestTest.java index ea793ff8db64..672fe13a1808 100644 --- a/src/test/java/org/junit/tests/running/classes/ParameterizedTestTest.java +++ b/src/test/java/org/junit/tests/running/classes/ParameterizedTestTest.java @@ -25,7 +25,10 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.Parameterized.UseParametersRunnerFactory; import org.junit.runners.model.InitializationError; +import org.junit.runners.parameterized.ParametersRunnerFactory; +import org.junit.runners.parameterized.TestWithParameters; public class ParameterizedTestTest { @RunWith(Parameterized.class) @@ -307,12 +310,10 @@ public void aTest() { } @Test - public void meaningfulFailureWhenParametersNotPublic() throws Exception { - Result result = JUnitCore.runClasses(ProtectedParametersTest.class); - String expected = String.format( - "No public static parameters method on class %s", - ProtectedParametersTest.class.getName()); - assertEquals(expected, result.getFailures().get(0).getMessage()); + public void meaningfulFailureWhenParametersNotPublic() { + assertTestCreatesSingleFailureWithMessage(ProtectedParametersTest.class, + "No public static parameters method on class " + + ProtectedParametersTest.class.getName()); } @RunWith(Parameterized.class) @@ -430,4 +431,42 @@ public void runsForEverySingleArgumentOfIterable() { .runClasses(SingleArgumentTestWithIterable.class); assertEquals(2, result.getRunCount()); } + + static public class ExceptionThrowingRunnerFactory implements + ParametersRunnerFactory { + public Runner createRunnerForTestWithParameters(TestWithParameters test) + throws InitializationError { + throw new InitializationError( + "Called ExceptionThrowingRunnerFactory."); + } + } + + @RunWith(Parameterized.class) + @UseParametersRunnerFactory(ExceptionThrowingRunnerFactory.class) + static public class TestWithUseParametersRunnerFactoryAnnotation { + @Parameters + public static Iterable data() { + return asList("single test"); + } + + public TestWithUseParametersRunnerFactoryAnnotation(Object argument) { + } + + @Test + public void aTest() { + } + } + + @Test + public void usesParametersRunnerFactoryThatWasSpecifiedByAnnotation() { + assertTestCreatesSingleFailureWithMessage( + TestWithUseParametersRunnerFactoryAnnotation.class, + "Called ExceptionThrowingRunnerFactory."); + } + + private void assertTestCreatesSingleFailureWithMessage(Class test, String message) { + Result result = JUnitCore.runClasses(test); + assertEquals(1, result.getFailures().size()); + assertEquals(message, result.getFailures().get(0).getMessage()); + } } \ No newline at end of file