Permalink
Browse files

SPR-10217 Implement JUnit 4 Support using Rules

Currently JUnit 4 support is provided by SpringJUnit4ClassRunner which
is a custom BlockJUnit4ClassRunner. There is no support for using other
runners like Theories or Parameterized or 3rd party runners like
MockitoJUnitRunner. A runner based approach does not seem to offer much
promise as runners are not composable, a custom Spring version of every
runner has to be developed and maintained.

With JUnit 4.9+ the preferred way to implement such behavior is to use
rules. Unlike runners there can be several ones of them and they can be
composed. In theory TestExecutionListener could be deprecated and be
replaced with standard JUnit rules but this seems to be a bit on the
drastic side.

This proposed implementation is using both a class rule and a method
rule. The class rule creates the TestContextManager, runs all the class
level callbacks and class level checks. The method rule runs all the
instance level callbacks and method level checks. I did not see a way
to implement the current functionality offered by
SpringJUnit4ClassRunner using only one rule.
Using two rules has the advantage that the implementation is cleaner
because it better separates the concerns. However it has the
disadvantage that it's harder to set up because both a method rule and
a class rule are needed. This also increases the potential for
misconfiguration.

The method rule has to be a MethodRule instead of a TestRule because
only the former has access to the test object with we need to perform
injection. This interface used to be deprecated once but doesn't seem
to be anymore. This creates a certain risk that it will be deprectated
again and potentially be remvoed in the future. An additional drawback
is that MethodRule unlike TestRule can only be defined in fields and
not methods. This is an unfortunate consequence of the implementation
of org.junit.runners.model.TestClass. As JUnit does not do
Field#setAccessible(true) this means that tests will have to be defined
in public fields.

Another minor issue is that tests not run because of IfProfileValue
will still show up in the Eclipse test tree, just blank.

In conclusion while the given implementation has some downsides I don't
see any other possible implementations given the current state of
affairs in JUnit.

- Add SpringJUnitClassRule for all the class level processing
- Add SpringJUnitMethodRule for all the method level processing
- Add tests for the rules

SPR-10217
  • Loading branch information...
1 parent f3ff98d commit d830cdc0bbc011c0056ba418f175d1414d616e9a @marschall committed Jan 23, 2013
View
149 spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnitClassRule.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2004-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit4;
+
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.springframework.test.annotation.ProfileValueUtils;
+import org.springframework.test.context.TestContextManager;
+
+
+/**
+ * <p>
+ * {@code SpringJUnitClassRule} is a custom {@link TestRule} which provides
+ * functionality of the <em>Spring TestContext Framework</em> to standard
+ * JUnit 4.9+ tests by means of the {@link TestContextManager} and associated
+ * support classes and annotations.
+ * </p>
+ * <p>
+ * Compared to {@link SpringJUnit4ClassRunner} the rule based JUnit support
+ * has the advantage that it is independent of the runner and can therefore
+ * be combined with existing 3rd party runners like {@code Parameterized}.
+ * </p>
+ * <p>
+ * However to achieve the same functionality as {@link SpringJUnit4ClassRunner}
+ * {@code SpringJUnitClassRule} has to be combined with
+ * {@link SpringJUnitMethodRule} as {@code SpringJUnitClassRule} only provides
+ * the class level features of {@link SpringJUnit4ClassRunner}.
+ * </p>
+ *
+ * <p>
+ * The following example shows you how to use {@code SpringJUnitClassRule}.
+ * </p>
+ * <pre><code>
+ * public class ExampleTest {
+ *
+ * @ClassRule
+ * public static final SpringJUnitClassRule CLASS_RULE = new SpringJUnitClassRule();
+ *
+ * @Rule
+ * public MethodRule methodRule = new SpringJUnitMethodRule(CLASS_RULE);
+ * }
+ * </code></pre>
+ *
+ * @author Philippe Marschall
+ * @since 3.2.2
+ * @see SpringJUnit4ClassRunner
+ * @see TestContextManager
+ * @see SpringJUnitMethodRule
+ */
+public class SpringJUnitClassRule implements TestRule {
+
+ // volatile since SpringJUnitMethodRule can potentially access it from a
+ // different thread depending on the runner.
+ private volatile TestContextManager testContextManager;
+
+ /**
+ * Get the {@link TestContextManager} associated with this runner.
+ * Will be {@code null} until this class is called by the JUnit framework.
+ */
+ protected final TestContextManager getTestContextManager() {
+ return this.testContextManager;
+ }
+
+
+ /**
+ * Creates a new {@link TestContextManager} for the supplied test class and
+ * the configured <em>default {@code ContextLoader} class name</em>.
+ * Can be overridden by subclasses.
+ * @param clazz the test class to be managed
+ * @see #getDefaultContextLoaderClassName(Class)
+ */
+ protected TestContextManager createTestContextManager(Class<?> clazz) {
+ return new TestContextManager(clazz, getDefaultContextLoaderClassName(clazz));
+ }
+
+
+ /**
+ * Get the name of the default {@code ContextLoader} class to use for
+ * the supplied test class. The named class will be used if the test class
+ * does not explicitly declare a {@code ContextLoader} class via the
+ * {@code &#064;ContextConfiguration} annotation.
+ * <p>The default implementation returns {@code null}, thus implying use
+ * of the <em>standard</em> default {@code ContextLoader} class name.
+ * Can be overridden by subclasses.
+ * @param clazz the test class
+ * @return {@code null}
+ */
+ protected String getDefaultContextLoaderClassName(Class<?> clazz) {
+ return null;
+ }
+
+
+ /**
+ * Check whether the test is enabled in the first place. This prevents
+ * classes with a non-matching {@code &#064;IfProfileValue} annotation
+ * from running altogether, even skipping the execution of
+ * {@code prepareTestInstance()} {@code TestExecutionListener}
+ * methods.
+ * Creates the {@link TestContextManager} and runs it's before and after
+ * class methods.
+ *
+ * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)
+ * @see org.springframework.test.annotation.IfProfileValue
+ * @see org.springframework.test.context.TestExecutionListener
+ * @see #createTestContextManager(Class)
+ * @see TestContextManager#beforeTestClass()
+ * @see TestContextManager#afterTestClass()
+ */
+ @Override
+ public Statement apply(final Statement base, final Description description) {
+ return new Statement() {
+
+ @Override
+ public void evaluate() throws Throwable {
+ Class<?> testClass = description.getTestClass();
+ boolean profileIsActive = ProfileValueUtils.isTestEnabledInThisEnvironment(testClass);
+ assumeTrue("required profile not active", profileIsActive);
+ testContextManager = createTestContextManager(testClass);
+ testContextManager.beforeTestClass();
+ try {
+ base.evaluate();
+ } finally {
+ testContextManager.afterTestClass();
+ // make test context manager eligible for garbage collection
+ testContextManager = null;
+ }
+
+ }
+ };
+ }
+
+}
View
97 spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnitMethodRule.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2004-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit4;
+
+import static org.junit.Assume.assumeTrue;
+
+import java.lang.reflect.Method;
+
+import org.junit.rules.MethodRule;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.springframework.test.annotation.ExpectedException;
+import org.springframework.test.annotation.ProfileValueUtils;
+import org.springframework.test.context.TestContextManager;
+
+
+/**
+ * <p>
+ * {@code MethodRule} is a custom {@link MethodRule} which together with
+ * {@link SpringJUnitClassRule} provides functionality of the <em>Spring
+ * TestContext Framework</em> to standard JUnit 4.9+ tests by means of the
+ * {@link TestContextManager} and associated support classes and annotations.
+ * </p>
+ * <p>
+ * The only feature not supported of {@link SpringJUnit4ClassRunner} is
+ * {@link ExpectedException}.
+ * </p>
+ *
+ * @author Philippe Marschall
+ * @since 3.2.2
+ * @see SpringJUnitClassRule
+ */
+public class SpringJUnitMethodRule implements MethodRule {
+
+ // Hold on to the class rule instead of the TestContextManager because
+ // the class rule "owns" the TestContextManager can releases it when no
+ // longer needed.
+ private final SpringJUnitClassRule classRule;
+
+ /**
+ * Constructs a new {@code SpringJUnitMethodRule}.
+ *
+ * @param classRule the class rule, not {@code null},
+ * the class rule has to be defined in the same test class
+ * where this {@link SpringJUnitMethodRule} instance is defined
+ */
+ public SpringJUnitMethodRule(SpringJUnitClassRule classRule) {
+ this.classRule = classRule;
+ }
+
+ /**
+ * Prepares the test instance (performs the injection) and runs the before
+ * and after test methods on the {@link TestContextManager}.
+ *
+ * @see TestContextManager#prepareTestInstance(Object)
+ * @see TestContextManager#beforeTestMethod(Object, Method)
+ * @see TestContextManager#afterTestMethod(Object, Method, Throwable)
+ */
+ @Override
+ public Statement apply(final Statement base, final FrameworkMethod method, final Object target) {
+ return new Statement() {
+
+ @Override
+ public void evaluate() throws Throwable {
+ Method testMethod = method.getMethod();
+ boolean profileIsActive = ProfileValueUtils.isTestEnabledInThisEnvironment(testMethod, target.getClass());
+ assumeTrue("required profile not active", profileIsActive);
+ TestContextManager testContextManager = classRule.getTestContextManager();
+ testContextManager.prepareTestInstance(target);
+ testContextManager.beforeTestMethod(target, testMethod);
+ try {
+ base.evaluate();
+ }
+ catch (Throwable t) {
+ testContextManager.afterTestMethod(target, testMethod, t);
+ throw t;
+ }
+ testContextManager.afterTestMethod(target, testMethod, null);
+ }
+ };
+ }
+
+}
View
3 spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java
@@ -1,6 +1,7 @@
/**
* <p>Support classes for ApplicationContext-based and transactional
- * tests run with JUnit 4.5+ and the <em>Spring TestContext Framework</em>.</p>
+ * tests run with JUnit 4.5+ and the <em>Spring TestContext Framework</em>.
+ * The rules need JUnit 4.9+</p>
*/
package org.springframework.test.context.junit4;
View
98 ...c/test/java/org/springframework/test/context/junit4/EnabledAndIgnoredSpringRuleTests.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2004-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit4;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.MethodRule;
+import org.springframework.test.annotation.IfProfileValue;
+import org.springframework.test.context.TestExecutionListeners;
+
+
+/**
+ * Same as {@link EnabledAndIgnoredSpringRunnerTests} but for rule based tests.
+ *
+ * @author Philippe Marschall
+ * @since 3.2.2
+ * @see EnabledAndIgnoredSpringRunnerTests
+ */
+@TestExecutionListeners( {})
+public class EnabledAndIgnoredSpringRuleTests {
+
+ @ClassRule
+ public static final SpringJUnitClassRule CLASS_RULE = new SpringJUnitClassRule();
+
+ @Rule
+ public MethodRule methodRule = new SpringJUnitMethodRule(CLASS_RULE);
+
+ protected static final String NAME = "EnabledAndIgnoredSpringRunnerTests.profile_value.name";
+
+ protected static final String VALUE = "enigma";
+
+ protected static int numTestsExecuted = 0;
+
+
+ @BeforeClass
+ public static void setProfileValue() {
+ numTestsExecuted = 0;
+ System.setProperty(NAME, VALUE);
+ }
+
+ @AfterClass
+ public static void verifyNumTestsExecuted() {
+ assertEquals("Verifying the number of tests executed.", 3, numTestsExecuted);
+ }
+
+ @Test
+ @IfProfileValue(name = NAME, value = VALUE + "X")
+ public void testIfProfileValueDisabled() {
+ numTestsExecuted++;
+ fail("The body of a disabled test should never be executed!");
+ }
+
+ @Test
+ @IfProfileValue(name = NAME, value = VALUE)
+ public void testIfProfileValueEnabledViaSingleValue() {
+ numTestsExecuted++;
+ }
+
+ @Test
+ @IfProfileValue(name = NAME, values = { "foo", VALUE, "bar" })
+ public void testIfProfileValueEnabledViaMultipleValues() {
+ numTestsExecuted++;
+ }
+
+ @Test
+ public void testIfProfileValueNotConfigured() {
+ numTestsExecuted++;
+ }
+
+ @Test
+ @Ignore
+ public void testJUnitIgnoreAnnotation() {
+ numTestsExecuted++;
+ fail("The body of an ignored test should never be executed!");
+ }
+
+}
View
52 ...test/java/org/springframework/test/context/junit4/IgnoredOnClassLevelSpringRuleTests.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2004-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit4;
+
+import static org.junit.Assert.fail;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.MethodRule;
+import org.springframework.test.annotation.IfProfileValue;
+import org.springframework.test.context.TestExecutionListeners;
+
+
+/**
+ * Same as {@link EnabledAndIgnoredSpringRuleTests} but on a class level.
+ *
+ * @author Philippe Marschall
+ * @since 3.2.2
+ * @see EnabledAndIgnoredSpringRuleTests
+ */
+@TestExecutionListeners( {})
+@IfProfileValue(name = EnabledAndIgnoredSpringRuleTests.NAME,
+ value = EnabledAndIgnoredSpringRuleTests.VALUE + "X")
+public class IgnoredOnClassLevelSpringRuleTests {
+
+ @ClassRule
+ public static final SpringJUnitClassRule CLASS_RULE = new SpringJUnitClassRule();
+
+ @Rule
+ public MethodRule methodRule = new SpringJUnitMethodRule(CLASS_RULE);
+
+ @Test
+ public void testIfProfileValueDisabled() {
+ fail("The body of a disabled test should never be executed!");
+ }
+
+}
View
9 ...src/test/java/org/springframework/test/context/junit4/JUnitRulesInjectionTest-context.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
+
+ <bean id="pet" class="org.springframework.tests.sample.beans.Pet">
+ <constructor-arg value="Z&#x00FC;sle" />
+ </bean>
+
+</beans>
View
88 ...g-test/src/test/java/org/springframework/test/context/junit4/JUnitRulesInjectionTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2004-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit4;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.MethodRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.tests.sample.beans.Pet;
+
+
+/**
+ * Verifies that injection works correctly for rule based JUnit 4 tests.
+ *
+ * @author Philippe Marschall
+ * @since 3.2.2
+ */
+@ContextConfiguration
+@RunWith(Parameterized.class)
+public class JUnitRulesInjectionTest {
+
+ @ClassRule
+ public static final SpringJUnitClassRule CLASS_RULE = new SpringJUnitClassRule();
+
+ @Rule
+ public MethodRule methodRule = new SpringJUnitMethodRule(CLASS_RULE);
+
+ private String parameter;
+
+ @Autowired
+ private ApplicationContext applicationContext;
+
+ @Autowired
+ private Pet pet;
+
+ public JUnitRulesInjectionTest(final String parameter) {
+ this.parameter = parameter;
+ }
+
+ @Test
+ public void firstMethod() {
+ assertNotNull("application context hasn't been injected", this.applicationContext);
+ assertNotNull("pet hasn't been injected", this.pet);
+ assertNotNull("parameter has not been set", this.parameter);
+ }
+
+ @Test
+ public void secondMethod() {
+ assertNotNull("application context hasn't been injected", this.applicationContext);
+ assertNotNull("pet hasn't been injected", this.pet);
+ assertNotNull("parameter has not been set", this.parameter);
+ }
+
+ @Parameters
+ public static Collection<Object[]> testData() {
+ return Arrays.asList(new Object[][] {//
+ //
+ { "first" },//
+ { "second" } //
+ });
+ }
+
+}
View
111 ...src/test/java/org/springframework/test/context/junit4/TestExecutionListenersRuleTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2004-2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit4;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.MethodRule;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.TestExecutionListener;
+import org.springframework.test.context.TestExecutionListeners;
+
+
+/**
+ * Verifies that test execution listeners work correctly for rule based
+ * JUnit 4 tests.
+ *
+ * @author Philippe Marschall
+ * @since 3.2.2
+ */
+@TestExecutionListeners(TestExecutionListenersRuleTest.InnerTestExecutionListener.class)
+public class TestExecutionListenersRuleTest {
+
+ @ClassRule
+ public static final SpringJUnitClassRule CLASS_RULE = new SpringJUnitClassRule();
+
+ @Rule
+ public MethodRule methodRule = new SpringJUnitMethodRule(CLASS_RULE);
+
+ private static int beforeTestClassRun = 0;
+ private static int prepareTestInstanceRun = 0;
+ private static int beforeTestMethodRun = 0;
+ private static int afterTestMethodRun = 0;
+ private static int afterTestClassRun = 0;
+
+ @BeforeClass
+ public static void verifyBeforeClass() {
+ assertEquals("beforeTestClassRun", 1, beforeTestClassRun);
+ assertEquals("prepareTestInstanceRun", 0, prepareTestInstanceRun);
+ assertEquals("beforeTestMethodRun", 0, beforeTestMethodRun);
+ assertEquals("afterTestMethodRun", 0, afterTestMethodRun);
+ assertEquals("afterTestClassRun", 0, afterTestClassRun);
+ }
+
+ @Test
+ public void verifyTest() {
+ assertEquals("beforeTestClassRun", 1, beforeTestClassRun);
+ assertEquals("prepareTestInstanceRun", 1, prepareTestInstanceRun);
+ assertEquals("beforeTestMethodRun", 1, beforeTestMethodRun);
+ assertEquals("afterTestMethodRun", 0, afterTestMethodRun);
+ assertEquals("afterTestClassRun", 0, afterTestClassRun);
+ }
+
+ @AfterClass
+ public static void verifyAfterClass() {
+ assertEquals("beforeTestClassRun", 1, beforeTestClassRun);
+ assertEquals("prepareTestInstanceRun", 1, prepareTestInstanceRun);
+ assertEquals("beforeTestMethodRun", 1, beforeTestMethodRun);
+ assertEquals("afterTestMethodRun", 1, afterTestMethodRun);
+ assertEquals("afterTestClassRun", 0, afterTestClassRun);
+ }
+
+
+ static class InnerTestExecutionListener implements TestExecutionListener {
+
+ @Override
+ public void beforeTestClass(TestContext testContext) throws Exception {
+ beforeTestClassRun += 1;
+ }
+
+ @Override
+ public void prepareTestInstance(TestContext testContext) throws Exception {
+ prepareTestInstanceRun += 1;
+ }
+
+ @Override
+ public void beforeTestMethod(TestContext testContext) throws Exception {
+ beforeTestMethodRun += 1;
+ }
+
+ @Override
+ public void afterTestMethod(TestContext testContext) throws Exception {
+ afterTestMethodRun += 1;
+ }
+
+ @Override
+ public void afterTestClass(TestContext testContext) throws Exception {
+ afterTestClassRun += 1;
+ }
+
+ }
+
+}

0 comments on commit d830cdc

Please sign in to comment.