From b7e6d12f7c4971d8c58628bf73dc762658462069 Mon Sep 17 00:00:00 2001 From: Alex Panchenko Date: Fri, 21 Apr 2017 02:19:06 +0200 Subject: [PATCH] @BeforeParam/@AfterParam for Parameterized runner (#1435) Closes #45 --- .../runners/statements/RunAfters.java | 9 +- .../runners/statements/RunBefores.java | 9 +- .../java/org/junit/runners/Parameterized.java | 132 +++++++-- .../BlockJUnit4ClassRunnerWithParameters.java | 44 ++- .../classes/ParameterizedTestTest.java | 276 +++++++++++++++++- 5 files changed, 442 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/junit/internal/runners/statements/RunAfters.java b/src/main/java/org/junit/internal/runners/statements/RunAfters.java index 7512a7d61f40..5e56c3350ac1 100644 --- a/src/main/java/org/junit/internal/runners/statements/RunAfters.java +++ b/src/main/java/org/junit/internal/runners/statements/RunAfters.java @@ -30,7 +30,7 @@ public void evaluate() throws Throwable { } finally { for (FrameworkMethod each : afters) { try { - each.invokeExplosively(target); + invokeMethod(each); } catch (Throwable e) { errors.add(e); } @@ -38,4 +38,11 @@ public void evaluate() throws Throwable { } MultipleFailureException.assertEmpty(errors); } + + /** + * @since 4.13 + */ + protected void invokeMethod(FrameworkMethod method) throws Throwable { + method.invokeExplosively(target); + } } \ No newline at end of file diff --git a/src/main/java/org/junit/internal/runners/statements/RunBefores.java b/src/main/java/org/junit/internal/runners/statements/RunBefores.java index 238fbe7d07a5..bd835c772530 100644 --- a/src/main/java/org/junit/internal/runners/statements/RunBefores.java +++ b/src/main/java/org/junit/internal/runners/statements/RunBefores.java @@ -21,8 +21,15 @@ public RunBefores(Statement next, List befores, Object target) @Override public void evaluate() throws Throwable { for (FrameworkMethod before : befores) { - before.invokeExplosively(target); + invokeMethod(before); } next.evaluate(); } + + /** + * @since 4.13 + */ + protected void invokeMethod(FrameworkMethod method) throws Throwable { + method.invokeExplosively(target); + } } \ No newline at end of file diff --git a/src/main/java/org/junit/runners/Parameterized.java b/src/main/java/org/junit/runners/Parameterized.java index cc7c804f4daa..34683cd83319 100644 --- a/src/main/java/org/junit/runners/Parameterized.java +++ b/src/main/java/org/junit/runners/Parameterized.java @@ -1,5 +1,6 @@ package org.junit.runners; +import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; @@ -8,11 +9,13 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import org.junit.runner.Runner; import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InvalidTestClassError; import org.junit.runners.model.TestClass; import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParametersFactory; import org.junit.runners.parameterized.ParametersRunnerFactory; @@ -134,6 +137,19 @@ * } * * + *

Executing code before/after executing tests for specific parameters

+ *

+ * If your test needs to perform some preparation or cleanup based on the + * parameters, this can be done by adding public static methods annotated with + * {@code @BeforeParam}/{@code @AfterParam}. Such methods should either have no + * parameters or the same parameters as the test. + *

+ * @BeforeParam
+ * public static void beforeTestsForParameter(String onlyParameter) {
+ *     System.out.println("Testing " + onlyParameter);
+ * }
+ * 
+ * *

Create different runners

*

* By default the {@code Parameterized} runner creates a slightly modified @@ -234,32 +250,91 @@ public class Parameterized extends Suite { Class value() default BlockJUnit4ClassRunnerWithParametersFactory.class; } + /** + * Annotation for {@code public static void} methods which should be executed before + * evaluating tests with particular parameters. + * + * @see org.junit.BeforeClass + * @see org.junit.Before + * @since 4.13 + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface BeforeParam { + } + + /** + * Annotation for {@code public static void} methods which should be executed after + * evaluating tests with particular parameters. + * + * @see org.junit.AfterClass + * @see org.junit.After + * @since 4.13 + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface AfterParam { + } + /** * Only called reflectively. Do not use programmatically. */ public Parameterized(Class klass) throws Throwable { - super(klass, RunnersFactory.createRunnersForClass(klass)); + this(klass, new RunnersFactory(klass)); + } + + private Parameterized(Class klass, RunnersFactory runnersFactory) throws Exception { + super(klass, runnersFactory.createRunners()); + validateBeforeParamAndAfterParamMethods(runnersFactory.parameterCount); + } + + private void validateBeforeParamAndAfterParamMethods(Integer parameterCount) + throws InvalidTestClassError { + List errors = new ArrayList(); + validatePublicStaticVoidMethods(Parameterized.BeforeParam.class, parameterCount, errors); + validatePublicStaticVoidMethods(Parameterized.AfterParam.class, parameterCount, errors); + if (!errors.isEmpty()) { + throw new InvalidTestClassError(getTestClass().getJavaClass(), errors); + } + } + + private void validatePublicStaticVoidMethods( + Class annotation, Integer parameterCount, + List errors) { + List methods = getTestClass().getAnnotatedMethods(annotation); + for (FrameworkMethod fm : methods) { + fm.validatePublicVoid(true, errors); + if (parameterCount != null) { + int methodParameterCount = fm.getMethod().getParameterTypes().length; + if (methodParameterCount != 0 && methodParameterCount != parameterCount) { + errors.add(new Exception("Method " + fm.getName() + + "() should have 0 or " + parameterCount + " parameter(s)")); + } + } + } } private static class RunnersFactory { private static final ParametersRunnerFactory DEFAULT_FACTORY = new BlockJUnit4ClassRunnerWithParametersFactory(); private final TestClass testClass; + private final FrameworkMethod parametersMethod; + private final List allParameters; + private final int parameterCount; - static List createRunnersForClass(Class klass) - throws Throwable { - return new RunnersFactory(klass).createRunners(); - } - private RunnersFactory(Class klass) { + private RunnersFactory(Class klass) throws Throwable { testClass = new TestClass(klass); + parametersMethod = getParametersMethod(testClass); + allParameters = allParameters(testClass, parametersMethod); + parameterCount = + allParameters.isEmpty() ? 0 : normalizeParameters(allParameters.get(0)).length; } - private List createRunners() throws Throwable { - Parameters parameters = getParametersMethod().getAnnotation( - Parameters.class); + private List createRunners() throws Exception { + Parameters parameters = parametersMethod.getAnnotation(Parameters.class); return Collections.unmodifiableList(createRunnersForParameters( - allParameters(), parameters.name(), + allParameters, parameters.name(), getParametersRunnerFactory())); } @@ -278,25 +353,37 @@ private ParametersRunnerFactory getParametersRunnerFactory() private TestWithParameters createTestWithNotNormalizedParameters( String pattern, int index, Object parametersOrSingleParameter) { - Object[] parameters = (parametersOrSingleParameter instanceof Object[]) ? (Object[]) parametersOrSingleParameter + Object[] parameters = normalizeParameters(parametersOrSingleParameter); + return createTestWithParameters(testClass, pattern, index, parameters); + } + + private static Object[] normalizeParameters(Object parametersOrSingleParameter) { + return (parametersOrSingleParameter instanceof Object[]) ? (Object[]) parametersOrSingleParameter : new Object[] { parametersOrSingleParameter }; - return createTestWithParameters(testClass, pattern, index, - parameters); } @SuppressWarnings("unchecked") - private Iterable allParameters() throws Throwable { - Object parameters = getParametersMethod().invokeExplosively(null); - if (parameters instanceof Iterable) { - return (Iterable) parameters; + private static List allParameters( + TestClass testClass, FrameworkMethod parametersMethod) throws Throwable { + Object parameters = parametersMethod.invokeExplosively(null); + if (parameters instanceof List) { + return (List) parameters; + } else if (parameters instanceof Collection) { + return new ArrayList((Collection) parameters); + } else if (parameters instanceof Iterable) { + List result = new ArrayList(); + for (Object entry : ((Iterable) parameters)) { + result.add(entry); + } + return result; } else if (parameters instanceof Object[]) { return Arrays.asList((Object[]) parameters); } else { - throw parametersMethodReturnedWrongType(); + throw parametersMethodReturnedWrongType(testClass, parametersMethod); } } - private FrameworkMethod getParametersMethod() throws Exception { + private static FrameworkMethod getParametersMethod(TestClass testClass) throws Exception { List methods = testClass .getAnnotatedMethods(Parameters.class); for (FrameworkMethod each : methods) { @@ -322,7 +409,7 @@ private List createRunnersForParameters( } return runners; } catch (ClassCastException e) { - throw parametersMethodReturnedWrongType(); + throw parametersMethodReturnedWrongType(testClass, parametersMethod); } } @@ -338,9 +425,10 @@ private List createTestsForParameters( return children; } - private Exception parametersMethodReturnedWrongType() throws Exception { + private static Exception parametersMethodReturnedWrongType( + TestClass testClass, FrameworkMethod parametersMethod) throws Exception { String className = testClass.getName(); - String methodName = getParametersMethod().getName(); + String methodName = parametersMethod.getName(); String message = MessageFormat.format( "{0}.{1}() must return an Iterable of arrays.", className, methodName); diff --git a/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java b/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java index ffed4beaf6bb..1b56b3a0788e 100644 --- a/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java +++ b/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java @@ -4,9 +4,12 @@ import java.lang.reflect.Field; import java.util.List; +import org.junit.internal.runners.statements.RunAfters; +import org.junit.internal.runners.statements.RunBefores; import org.junit.runner.RunWith; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.model.FrameworkField; import org.junit.runners.model.FrameworkMethod; @@ -135,7 +138,46 @@ protected void validateFields(List errors) { @Override protected Statement classBlock(RunNotifier notifier) { - return childrenInvoker(notifier); + Statement statement = childrenInvoker(notifier); + statement = withBeforeParams(statement); + statement = withAfterParams(statement); + return statement; + } + + private Statement withBeforeParams(Statement statement) { + List befores = getTestClass() + .getAnnotatedMethods(Parameterized.BeforeParam.class); + return befores.isEmpty() ? statement : new RunBeforeParams(statement, befores); + } + + private class RunBeforeParams extends RunBefores { + RunBeforeParams(Statement next, List befores) { + super(next, befores, null); + } + + @Override + protected void invokeMethod(FrameworkMethod method) throws Throwable { + int paramCount = method.getMethod().getParameterTypes().length; + method.invokeExplosively(null, paramCount == 0 ? (Object[]) null : parameters); + } + } + + private Statement withAfterParams(Statement statement) { + List afters = getTestClass() + .getAnnotatedMethods(Parameterized.AfterParam.class); + return afters.isEmpty() ? statement : new RunAfterParams(statement, afters); + } + + private class RunAfterParams extends RunAfters { + RunAfterParams(Statement next, List afters) { + super(next, afters, null); + } + + @Override + protected void invokeMethod(FrameworkMethod method) throws Throwable { + int paramCount = method.getMethod().getParameterTypes().length; + method.invokeExplosively(null, paramCount == 0 ? (Object[]) null : parameters); + } } @Override 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 14916c6f5565..b7d7cbb260d7 100644 --- a/src/test/java/org/junit/tests/running/classes/ParameterizedTestTest.java +++ b/src/test/java/org/junit/tests/running/classes/ParameterizedTestTest.java @@ -3,8 +3,8 @@ import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.experimental.results.PrintableResult.testResult; @@ -12,10 +12,13 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.Description; import org.junit.runner.JUnitCore; @@ -24,6 +27,7 @@ import org.junit.runner.RunWith; import org.junit.runner.Runner; import org.junit.runner.notification.Failure; +import org.junit.runners.MethodSorters; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @@ -259,6 +263,228 @@ public void beforeAndAfterClassAreRun() { assertEquals("before after ", fLog); } + @RunWith(Parameterized.class) + @FixMethodOrder(MethodSorters.NAME_ASCENDING) + public static class BeforeParamAndAfterParam { + @BeforeClass + public static void before() { + fLog += "beforeClass "; + } + + @Parameterized.BeforeParam + public static void beforeParam(String x) { + fLog += "before(" + x + ") "; + } + + @Parameterized.AfterParam + public static void afterParam() { + fLog += "afterParam "; + } + + @AfterClass + public static void after() { + fLog += "afterClass "; + } + + private final String x; + + public BeforeParamAndAfterParam(String x) { + this.x = x; + } + + @Parameters + public static Collection data() { + return Arrays.asList("A", "B"); + } + + @Test + public void first() { + fLog += "first(" + x + ") "; + } + + @Test + public void second() { + fLog += "second(" + x + ") "; + } + } + + @Test + public void beforeParamAndAfterParamAreRun() { + fLog = ""; + Result result = JUnitCore.runClasses(BeforeParamAndAfterParam.class); + assertEquals(0, result.getFailureCount()); + assertEquals("beforeClass before(A) first(A) second(A) afterParam " + + "before(B) first(B) second(B) afterParam afterClass ", fLog); + } + + @RunWith(Parameterized.class) + @FixMethodOrder(MethodSorters.NAME_ASCENDING) + public static class MultipleBeforeParamAndAfterParam { + @Parameterized.BeforeParam + public static void before1() { + fLog += "before1() "; + } + + @Parameterized.BeforeParam + public static void before2(String x) { + fLog += "before2(" + x + ") "; + } + + @Parameterized.AfterParam + public static void after2() { + fLog += "after2() "; + } + + @Parameterized.AfterParam + public static void after1(String x) { + fLog += "after1(" + x + ") "; + } + + private final String x; + + public MultipleBeforeParamAndAfterParam(String x) { + this.x = x; + } + + @Parameters + public static Collection data() { + return Arrays.asList("A", "B"); + } + + @Test + public void first() { + fLog += "first(" + x + ") "; + } + + @Test + public void second() { + fLog += "second(" + x + ") "; + } + } + + @Test + public void multipleBeforeParamAndAfterParamAreRun() { + fLog = ""; + Result result = JUnitCore.runClasses(MultipleBeforeParamAndAfterParam.class); + assertEquals(0, result.getFailureCount()); + assertEquals("before1() before2(A) first(A) second(A) after1(A) after2() " + + "before1() before2(B) first(B) second(B) after1(B) after2() ", fLog); + } + + @RunWith(Parameterized.class) + @FixMethodOrder(MethodSorters.NAME_ASCENDING) + public static class MultipleParametersBeforeParamAndAfterParam { + @Parameterized.BeforeParam + public static void before(String x, int y) { + fLog += "before(" + x + "," + y + ") "; + } + + @Parameterized.AfterParam + public static void after(String x, int y) { + fLog += "after(" + x + "," + y + ") "; + } + + private final String x; + private final int y; + + public MultipleParametersBeforeParamAndAfterParam(String x, int y) { + this.x = x; + this.y = y; + } + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[]{"A", 1}, new Object[]{"B", 2}); + } + + @Test + public void first() { + fLog += "first(" + x + "," + y + ") "; + } + + @Test + public void second() { + fLog += "second(" + x + "," + y + ") "; + } + } + + @Test + public void multipleParametersBeforeParamAndAfterParamAreRun() { + fLog = ""; + Result result = JUnitCore.runClasses(MultipleParametersBeforeParamAndAfterParam.class); + assertEquals(0, result.getFailureCount()); + assertEquals("before(A,1) first(A,1) second(A,1) after(A,1) " + + "before(B,2) first(B,2) second(B,2) after(B,2) ", fLog); + } + + @RunWith(Parameterized.class) + public static class BeforeParamAndAfterParamError { + @Parameterized.BeforeParam + public void beforeParam(String x) { + } + + @Parameterized.AfterParam + private static void afterParam() { + } + + public BeforeParamAndAfterParamError(String x) { + } + + @Parameters + public static Collection data() { + return Arrays.asList("A", "B"); + } + + @Test + public void test() { + } + } + + @Test + public void beforeParamAndAfterParamValidation() { + fLog = ""; + Result result = JUnitCore.runClasses(BeforeParamAndAfterParamError.class); + assertEquals(1, result.getFailureCount()); + List failures = result.getFailures(); + assertThat(failures.get(0).getMessage(), containsString("beforeParam() should be static")); + assertThat(failures.get(0).getMessage(), containsString("afterParam() should be public")); + } + + @RunWith(Parameterized.class) + public static class BeforeParamAndAfterParamErrorNumberOfParameters { + @Parameterized.BeforeParam + public static void beforeParam(String x, String y) { + } + + @Parameterized.AfterParam + public static void afterParam(String x, String y, String z) { + } + + public BeforeParamAndAfterParamErrorNumberOfParameters(String x) { + } + + @Parameters + public static Collection data() { + return Arrays.asList("A", "B", "C", "D"); + } + + @Test + public void test() { + } + } + + @Test + public void beforeParamAndAfterParamValidationNumberOfParameters() { + fLog = ""; + Result result = JUnitCore.runClasses(BeforeParamAndAfterParamErrorNumberOfParameters.class); + assertEquals(1, result.getFailureCount()); + List failures = result.getFailures(); + assertThat(failures.get(0).getMessage(), + containsString("Method beforeParam() should have 0 or 1 parameter(s)")); + assertThat(failures.get(0).getMessage(), + containsString("Method afterParam() should have 0 or 1 parameter(s)")); + } + @RunWith(Parameterized.class) static public class EmptyTest { @BeforeClass @@ -408,9 +634,14 @@ public void runsForEverySingleArgumentOfArray() { @RunWith(Parameterized.class) static public class SingleArgumentTestWithIterable { + private static final AtomicBoolean dataCalled = new AtomicBoolean(false); + @Parameters public static Iterable data() { - return asList("first test", "second test"); + if (!dataCalled.compareAndSet(false, true)) { + fail("Should not call @Parameters method more than once"); + } + return new OneShotIterable(asList("first test", "second test")); } public SingleArgumentTestWithIterable(Object argument) { @@ -421,6 +652,22 @@ public void aTest() { } } + private static class OneShotIterable implements Iterable { + private final Iterable delegate; + private final AtomicBoolean iterated = new AtomicBoolean(false); + + OneShotIterable(Iterable delegate) { + this.delegate = delegate; + } + + public Iterator iterator() { + if (iterated.compareAndSet(false, true)) { + return delegate.iterator(); + } + throw new IllegalStateException("Cannot call iterator() more than once"); + } + } + @Test public void runsForEverySingleArgumentOfIterable() { Result result= JUnitCore @@ -428,6 +675,29 @@ public void runsForEverySingleArgumentOfIterable() { assertEquals(2, result.getRunCount()); } + @RunWith(Parameterized.class) + static public class SingleArgumentTestWithCollection { + @Parameters + public static Iterable data() { + return Collections.unmodifiableCollection(asList("first test", "second test")); + } + + public SingleArgumentTestWithCollection(Object argument) { + } + + @Test + public void aTest() { + } + } + + @Test + public void runsForEverySingleArgumentOfCollection() { + Result result= JUnitCore + .runClasses(SingleArgumentTestWithCollection.class); + assertEquals(2, result.getRunCount()); + } + + static public class ExceptionThrowingRunnerFactory implements ParametersRunnerFactory { public Runner createRunnerForTestWithParameters(TestWithParameters test) @@ -493,4 +763,4 @@ public void usesParametersRunnerFactoryThatWasSpecifiedByAnnotationInSuperClass( UseParameterizedFactoryTest.class, "Called ExceptionThrowingRunnerFactory."); } -} \ No newline at end of file +}