This article uses the JUnit4.12 Spring test5.0.7.RELEASE source code
Before because of
We recently encountered a problem. A common unit test of a Spring Boot project can normally use Spring Test features, such as dependency injection, transaction management, etc. The features of Spring Test no longer apply. Looked through online Chinese language materials, there is no perfect solution, is more of a simple call TestContextManager. PrepareTestInstance () to realize the Spring initialization, not solve the transaction management, etc. The Spring test feature is not available. For this reason, I decided to take a good look at the source code of JUnit4 and Spring test to understand their implementation and extension mechanisms and find a way to completely solve these problems.
The above questions can be divided into four sub-questions, which are:
- How does JUnit4 work internally
- How to extend Spring test on JUnit4
- What are the differences between Parameterized, Suite, and BlockJUnit4ClassRunner
- How to combine parameterized tests with Spring Test
1. How does JUnit4 work internally
(1) Introduction of key classesCopy the code
Runner describes how a test case should be executed in general. At its core is the run(RunNotifier) method, where the RunNotifier is used to publish notifications. ParentRunner inherits from Runner
Provides most of the functionality specific to a Runner that implements a “parent node” in the test tree, With children defined by objects of some data type T. (For BlockJUnit4ClassRunner, T is Method. T is Class.)
For the BlockJUnit4ClassRunner, T is Method. For the BlockJUnit4ClassRunner, T is Method. The Suite T is the default Runner for Class BlockJUnit4ClassRunner JUnit4. The Suite T is the default Runner for Class BlockJUnit4ClassRunner JUnit4. The Suite T is the default Runner for Class BlockJUnit4ClassRunner JUnit4. It has only one method evaluate() RunnerBuilder that describes how to build a set of runners JUnitCore where JUnit4 started
(2) The running process of a unit testCopy the code
Usually, the IDE manually runs the unit test to check the defect rate of the case, and the code coverage is always taken by the IDE such as Eclipse/Intellij IDEA as the main entry, and then calls the Runner of run(Request) to obtain the test case
public Result run(Request request) {
return run(request.getRunner());
}
Copy the code
The RunnerBuilder will then be found by default in the order IgnoredBuilder, AnnotatedBuilder, SuiteMethodBuilder, Junit3Builder, Junit4Builder
@Override
public Runner runnerForClass(Class
testClass) throws Throwable {List<RunnerBuilder> builders = arrays.aslist (ignoredBuilder(), annotatedBuilder(), suiteMethodBuilder(), Junit3Builder (), junit4Builder ());for (RunnerBuilder each : builders) {
Runner runner = each.safeRunnerForClass(testClass);
if(runner ! =null) {
returnrunner; }}return null;
}
Copy the code
IgnoredBuilder has the highest priority, looking for the @ignore annotation on the test class, creating IgnoredClassRunner, and the test case will be ignored by AnnotatedBuilder, which is the case for most of our test cases. It looks for the @runWith annotation on the test Class, calls the (Class) constructor, and calls the (Class, RunnerBuilder) constructor if it can’t find it
@Override
public Runner runnerForClass(Class
testClass) throws Exception {
for(Class<? > currentTestClass = testClass; currentTestClass ! =null;
currentTestClass = getEnclosingClassForNonStaticMemberClass(currentTestClass)) {
RunWith annotation = currentTestClass.getAnnotation(RunWith.class);
if(annotation ! =null) {
returnbuildRunner(annotation.value(), testClass); }}return null;
}
public Runner buildRunner(Class
runnerClass, Class
testClass) throws Exception {
try {
return runnerClass.getConstructor(Class.class).newInstance(testClass);
} catch (NoSuchMethodException e) {
try {
return runnerClass.getConstructor(Class.class,
RunnerBuilder.class).newInstance(testClass, suiteBuilder);
} catch (NoSuchMethodException e2) {
String simpleName = runnerClass.getSimpleName();
throw newInitializationError(String.format( CONSTRUCTOR_ERROR_FORMAT, simpleName, simpleName)); }}}Copy the code
Then go to SuiteMethodBuilder and look for a method named Suite. Junit3Builder will determine if the test class is a subclass of TestCase for compatibility with JUnit3. If none of the above is true, Junit4Builder will be used. By default, BlockJUnit4ClassRunner is used, so if our test cases do not have additional annotations, BlockJUnit4ClassRunner will be used
After determining the Runner, JUnitCore executes run(Runner), calling Runner. Run (Notifier)
public Result run(Runner runner) {
Result result = new Result();
RunListener listener = result.createListener();
notifier.addFirstListener(listener);
try {
notifier.fireTestRunStarted(runner.getDescription());
runner.run(notifier);
notifier.fireTestRunFinished(result);
} finally {
removeListener(listener);
}
return result;
}
Copy the code
Different runners have different implementation methods. The following takes the default BlockJUnit4ClassRunner as an example to analyze that BlockJUnit4ClassRunner does not override the run() method, so it calls the parent trunner.run (). The method logic is simple. ClassBlock () constructs a statement, and then executes the statement’s evaluate().
@Override
public void run(final RunNotifier notifier) {
EachTestNotifier testNotifier = newEachTestNotifier (notifier, getDescription ());try {
Statement statement = classBlock(notifier);
statement.evaluate();
} catch (AssumptionViolatedException e) {
testNotifier.addFailedAssumption(e);
} catch (StoppedByUserException e) {
throw e;
} catch(Throwable e) { testNotifier.addFailure(e); }}Copy the code
Let’s look at classBlock
protected Statement classBlock(final RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if(! areAllChildrenIgnored()) { statement = withBeforeClasses(statement); statement = withAfterClasses(statement); statement = withClassRules(statement); }return statement;
}
Copy the code
ChildrenInvoker () constructs a statement, executes the runChildren() method, which is mainly runChildren() because it is ParentRunner, and withBeforeClasses(), The role of withAfterClasses() will be analyzed later
private void runChildren(final RunNotifier notifier) {
final RunnerScheduler currentScheduler = scheduler;
try {
for (final T each : getFilteredChildren()) {
currentScheduler.schedule(new Runnable() {
public void run(a) {
ParentRunner.this.runChild(each, notifier); }}); }}finally{ currentScheduler.finished(); }}Copy the code
The logic is pretty simple, getting the list of Children based on getChildren() and then calling runChild() one by one. GetChildren () and runChild() are not implemented in ParentRunner, so let’s look at the implementation of the one-shot class BlockJUnit4ClassRunner
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
if (isIgnored(method)) {
notifier.fireTestIgnored(description);
} else{ runLeaf(methodBlock(method), description, notifier); }}Copy the code
GetChildren () looks for @test annotated methods in the Test class and runChild() wraps each child(method in the BlockJUnit4ClassRunner) with a call methodBlock(), The entire test method is then executed by calling statement.evaluate()
The BlockJUnit4ClassRunner runs as follows
protected Statement methodBlock(FrameworkMethod method) {
Object test;
try {
test = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall(a) throws Throwable {
return createTest();
}
}.run();
} catch (Throwable e) {
return new Fail(e);
}
Statement statement = methodInvoker(method, test);
statement = possiblyExpectingExceptions(method, test, statement);
statement = withPotentialTimeout(method, test, statement);
statement = withBefores(method, test, statement);
statement = withAfters(method, test, statement);
statement = withRules(method, test, statement);
return statement;
}
Copy the code
We can see that classBlock() and methodBlock() constantly wrap statement, and the statement returned by the previous method is used as an argument to the next method. This design pattern is very similar to the chain of responsibility, the Struts2 interceptor. Each wrapper takes statement as the argument next and then calls its own logic assuming that there is a TestClass called TestClass that has two test methods. TestMethodA () and testMethodB(), respectively, so the diagram of the test class is shown below
- ClassRules corresponds to the @classRule annotation
- AfterClasses corresponds to the @AfterClass annotation
- BeforeClasses corresponds to the @beforeClass annotation
- Rules corresponds to the @rule annotation
- Afters corresponds to the @classRule annotation
- Beofres corresponds to the @classRule annotation
- PotentialTimeout corresponds to the timeout property of the @test annotation
- ExpectingExceptions corresponds to the expected attribute in the @test annotation
- MethodInvoker corresponds to the test method called by reflection
This is exactly the same as using JUnit4 annotations, so far we have seen the inner workings of JUnit4
2. How to extend Spring test on JUnit4
So how does Spring, the core framework for Java development, extend its test features to JUnit4? The key is the TestContextManager and the SpringJUnit4ClassRunner(or SpringRunner), which inherits from the BlockJUnit4ClassRunner and overwrites some of the methods
@Override
protected Statement methodBlock(FrameworkMethod frameworkMethod) {
Object testInstance;
try {
testInstance = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall(a) throws Throwable {
return createTest();
}
}.run();
}
catch (Throwable ex) {
return new Fail(ex);
}
Statement statement = methodInvoker(frameworkMethod, testInstance);
statement = withBeforeTestExecutionCallbacks(frameworkMethod, testInstance, statement);
statement = withAfterTestExecutionCallbacks(frameworkMethod, testInstance, statement);
statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement);
statement = withBefores(frameworkMethod, testInstance, statement);
statement = withAfters(frameworkMethod, testInstance, statement);
statement = withRulesReflectively(frameworkMethod, testInstance, statement);
statement = withPotentialRepeat(frameworkMethod, testInstance, statement);
statement = withPotentialTimeout(frameworkMethod, testInstance, statement);
return statement;
}
Copy the code
We can see him with BlockJUnit4ClassRunner. MethodBlock () is roughly same, the difference is that he added BeforeTestExecution, AfterTestExecution and PotentialRepeat, The first two are wrapper points added by Spring test itself, which are not in the default wrapper provided by JUnit4. The latter is support for the @repeat annotation. In addition, Spring test also provides TestExecutionListener. The seven interface methods correspond to the seven wrapping points of the test case
- PrepareTestInstance () corresponds to the preparation for instance initialization
- BeforeTestClass () is after the @beforeClass execution point
- BeforeTestMethod () is after the @before execution point, such as starting transactions
- BeforeTestExecution () before the test method is closest to execution
- AfterTestExecution () is after ExpectException execution point, closest to after test method execution,
- AfterTestMethod () is After the @After point of execution, such as committing or rolling back transactions
- AfterTestClass () is after the @AfterClass execution point
Spring’s TestContextManager is created when the SpringJUnit4ClassRunner is initialized by JUnitCore. He will find the testExecutionListeners defined in the meta-INF /spring.factories in the classpath. The Spring Boot project will usually find 12, each in the spring-test factory. Spring-boot-test and Spring-boot-test-AutoConfigure jars. Therefore, if you want to customize TestExecutionListener according to the project requirements, you only need to design the TestExecutionListener as described above and inject it into the life cycle of the test case
TransactionalTestExecutionListener
@Override
public void beforeTestMethod(final TestContext testContext) throws Exception { Method testMethod = testContext.getTestMethod(); Class<? > testClass = testContext.getTestClass();// Omit part of the codetm = getTransactionManager(testContext, transactionAttribute.getQualifier()); Assert.state(tm ! =null, () - >"Failed to retrieve PlatformTransactionManager for @Transactional test: " + testContext);
}
// Start the transaction
if(tm ! =null) {
txContext = newTransactionContext(testContext, tm, transactionAttribute, isRollback(testContext)); runBeforeTransactionMethods(testContext); txContext.startTransaction(); TransactionContextHolder.setCurrentTransactionContext(txContext); }}Copy the code
He implements beforeTestMethod() and afterTestMethod() in which transactions are started/committed/rolled back
To sum up, SpringJUnit4ClassRunner has so many processes to run in a statement (I’m impressed that I drew them myself).
3. What are the differences between Parameterized, Suite, and BlockJUnit4ClassRunner
In JUnit4, @runWith is often used with either the BlockJUnit4ClassRunner or Parameterized parameter. BlockJUnit4ClassRunner is described above
Parameterized inherits from Suite, which is used to run multiple test classes together. Parameterized is a test class that executes multiple sets of parameters multiple times. It is essentially similar to Suite
Suite inherits from ParentRunner. Note that the children of Suite are runners, which is higher than that of BlockJUnit4ClassRunner. So getChildren() for Suite is returning runners, and runChild() is calling run() on runner
@Override
protected List<Runner> getChildren(a) {
return runners;
}
@Override
protected void runChild(Runner runner, final RunNotifier notifier) {
runner.run(notifier);
}
Copy the code
The difference between Parameterized and Suite is that Children of Suite is the Runner set corresponding to the test class set. The Parameterized according to how many set of parametric array, as many groups as building BlockJUnit4ClassRunnerWithParameters, then each set of parametric array into each Runner, after can like Suite runChild ()
4. How to make parametric test and Spring Test perfect combination
Combined with described above Spring test and the Parameterized, the parametric test Runner is BlockJUnit4ClassRunnerWithParameters default, It inherits from the BlockJUnit4ClassRunner implementation of injecting parameters without the features of Spring Test
A paragraph in the Parameterized source code comment gives us a clue
By default the Parameterized runner creates a slightly modified BlockJUnit4ClassRunner for each set of parameters. You can build an own Parameterized runner that creates another runner for each set of parameters. Therefore you have to build a ParametersRunnerFactory that creates a runner for each TestWithParameters. (TestWithParameters are bundling the parameters and the test name.) The factory must have a public zero-arg constructor. Use the Parameterized.UseParametersRunnerFactory to tell the Parameterized runner that it should use your factory.
If you want to customize Parameterized Runner, re-implement ParametersRunnerFactory and build a Runner that can inject parameters. Then use the @ UseParametersRunnerFactory annotations to specify a custom factory.
So we only need to design a class SpringJUnit4ClassRunnerWithParametersFactory inheritance SpringJUnit4ClassRunner, ensure support Spring characteristics of the test, then rejoined the function of the injection parameters, This part of the function can be done reference BlockJUnit4ClassRunnerWithParameters.
We found that in the process of implementation of SpringJUnit4ClassRunner BlockJUnit4ClassRunnerWithParameters and rewrite the createTest () method, then we need to put the two methods together The final effect is as follows
@Override
public Object createTest(a) throws Exception {
Object testInstance;
if (fieldsAreAnnotated()) {
testInstance = createTestUsingFieldInjection();
} else {
testInstance = createTestUsingConstructorInjection();
}
getTestContextManager().prepareTestInstance(testInstance);
return testInstance;
}
Copy the code
While this is not the only solution, Spring Test provides a more general solution by adding Spring Test functionality to the original BlockJUnit4ClassRunner’s Rules and ClassRules
@ClassRule
public static final SpringClassRule springClassRule = new SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
Copy the code
But this method has a drawback, because TestExecutionListener interface defines the 3 sets of injection point, junit 4 provides only two injection, for there is no injection before/afterTestExecution, be careful!
The following is also the official source code to remind us (poking fun at JUnit4’s shortcomings)
WARNING: Due to the shortcomings of JUnit rules, the SpringMethodRule does not support the beforeTestExecution() and afterTestExecution() callbacks of the TestExecutionListener API.
Fortunately, the Spring test provided by 12 TestExecutionListener did not use before/afterTestExecution, so that the 12 TestExecutionListener are still works in this way
What are you talking about
The source code for JUnit4 and Spring test is relatively easy to understand, and I have to say that debugging + stack + comments are really the three magic keys to understanding the source code. Of course, JUnit4 and Spring test are much more than that. This article is just about extracting the key designs. Technology is a huge hole that gets deeper and deeper
While reading the source code, I also found some interesting places. SpringJUnit4ClassRunner will find the withRules method of JUnit4 and change it to public with reflection when loading, as if to joke that JUnit4 does not make this method open, but Spring wants to extend this method
static {
Assert.state(ClassUtils.isPresent("org.junit.internal.Throwables", SpringJUnit4ClassRunner.class.getClassLoader()),
"SpringJUnit4ClassRunner requires JUnit 4.12 or higher.");
Method method = ReflectionUtils.findMethod(SpringJUnit4ClassRunner.class, "withRules", FrameworkMethod.class, Object.class, Statement.class); Assert.state(method ! =null."SpringJUnit4ClassRunner requires JUnit 4.12 or higher");
ReflectionUtils.makeAccessible(method);
withRulesMethod = method;
}
Copy the code
As for why JUnit4 is analyzed, the spring-boot-starter test introduces JUnit4 by default, and I don’t know why JUnit5 is not. If anyone knows why, please leave a private message to tell me. Based on the new features of JUnit5, I will probably use it later, and I will write another article if I find something new
Thank you very much for reading this article. If you find it helpful, please pay attention to it