forked from spring-projects/spring-framework
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
8 changed files
with
606 additions
and
1 deletion.
There are no files selected for viewing
149 changes: 149 additions & 0 deletions
149
spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnitClassRule.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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 @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 @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; | |||
} | |||
|
|||
} | |||
}; | |||
} | |||
|
|||
} |
97 changes: 97 additions & 0 deletions
97
spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnitMethodRule.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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); | |||
} | |||
}; | |||
} | |||
|
|||
} |
3 changes: 2 additions & 1 deletion
3
spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
...c/test/java/org/springframework/test/context/junit4/EnabledAndIgnoredSpringRuleTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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!"); | |||
} | |||
|
|||
} |
Oops, something went wrong.