Junit was started as a framework by Kent Beck and Erich Gamma in late 1995. Since then, the Junit framework has grown in popularity and is now the de facto standard for unit testing Java applications.

Never in the world of software development has a few lines of code played such an important role in a lot of code — Martin Fowler

Start with a simple example

This paper focuses on the basic principles of Junit operation and the process of executing unit tests. Therefore, additional information and data are not prepared separately. The following test cases are used in this paper:

package com.glmapper.bridge.boot;

import org.junit.*;

public class JunitSamplesTest {

    @Before
    public void before(a){
        System.out.println("... this is before test......");
    }

    @After
    public void after(a){
        System.out.println("... this is after test......");
    }

    @BeforeClass
    public static void beforeClass(a){
        System.out.println("... this is before class test......");
    }

    @AfterClass
    public static void afterClass(a){
        System.out.println("... this is after class test......");
    }

    @Test
    public void testOne(a){
        System.out.println("this is test one");
    }

    @Test
    public void testTwo(a){
        System.out.println("this is test two"); }}Copy the code

The execution result is as follows:

.this is before class test.Disconnected from the target VM.address: '127.0.0.1:65400', transport: 'socket'...this is before test.this is test one.this is after test. .this is before test.this is test two.this is after test. .this is after class test.Copy the code

In terms of code and execution results, BeforeClass and AfterClass annotations are executed Before and After the test class starts, respectively, and Before and After annotations are executed Before and After each test method in the test class.

The problem domain

From a developer’s point of view, using any component of a technology product better means understanding it. As you can see from the case provided above, Junit is very easy to use. You can get started by annotating the Test method with @test, placing the logic to be tested in the @test annotated method, and then running it. Simplicity comes from the component developer’s top-level abstraction and encapsulation, masking the technical details and presenting them to the user with the simplest API or annotations. This is the fundamental reason Junit is so easy for developers to accept.

Back to the previous analysis, Junit is easy to use because it provides a very concise API and annotations, so for us, these are the basic starting points for analyzing Junit; With this in mind, let’s break away from the fundamentals of Junit. Based on the small case in section 1, here are some questions:

  • How does Junit trigger execution
  • Why do methods annotated with @test get executed, but unannotated methods don’t
  • Before and After execution timing
  • BeforeClass and AfterClass Execution timing
  • How Junit collects execution results and returns them (not focusing on IDE rendering here)

How is Junit executed?

Here, type the breakpoint directly at the target test method location and debug executes

Find the entire path through the use case execution through the stack. Since this case is executed through idea, you can see that the entry is actually wrapped by idea. But there is also an entry point to JUnitCore.

JUnitCore is the facade entry point for running test cases. As you can see from the source code comments, JUnitCore is available from junit 4, but it is backward compatible with the 3.8.x version family. When we run test cases, most of the time we run them locally through the IDE, or through the MVN test. In fact, both the IDE and MVN are packages of JUnitCore. We can do this by using the main method. For example, run the main method of the following code to pass a JUnitCore instance, and then specify the class being tested to trigger the use case execution. In order to make the stack as close to Junit’s own code as possible, We start in this way to reduce stack interference with the code execution path.

public class JunitSamplesTest {

    @Before
    public void before(a){
        System.out.println("... this is before test......");
    }

    @After
    public void after(a){
        System.out.println("... this is after test......");
    }

    @BeforeClass
    public static void beforeClass(a){
        System.out.println("... this is before class test......");
    }

    @AfterClass
    public static void afterClass(a){
        System.out.println("... this is after class test......");
    }

    @Test
    public void testOne(a){
        System.out.println("this is test one");
    }

    @Test
    public void testTwo(a){
        System.out.println("this is test two");
    }

    public static void main(String[] args) {
        JUnitCore jUnitCore = newJUnitCore(); jUnitCore.run(JunitSamplesTest.class); }}Copy the code

Here is the simplest test execution entry:

If you use a Java command to boot, you actually start with JunitCore’s own main method

/** * Run the tests contained in the classes named in the args. If all tests run successfully, exit with a status of 0. Otherwise exit with a status of 1. Write * feedback while tests are running and write stack traces for all failed tests after the tests all complete. * Params: * args -- Names of classes in which to find tests to run **/

public static void main(String... args) {
    Result result = new JUnitCore().runMain(new RealSystem(), args);
    System.exit(result.wasSuccessful() ? 0 : 1);
}

Copy the code

Why do methods annotated with @test get executed, but unannotated methods don’t

The @test annotation must have been detected by Junit in some way and placed in a collection or queue to be executed. Let’s examine the code to demonstrate this.

org.junit.runners.BlockJUnit4ClassRunner#getChildren

@Override
protected List<FrameworkMethod> getChildren(a) {
    return computeTestMethods();
}
Copy the code

The purpose of the method computeTestMethods is to compute all the test methods.

EtAnnotatedMethods filters all annotated methods in the current TestClass with type annotationClass by specifying the annotationClass type.

GetFilteredChildren finally cache the obtained test method in filteredChildren. Here’s a quick summary of how the @test annotation was identified (other annotations like @before are the same)

  • 1. During the initialization of Runner, Junit internally creates a TestClass object model based on the given TestClass, which describes the representation of the current TestClass in Junit.
// Clazz is the class to test
public TestClass(Class
        clazz) {
    this.clazz = clazz;
    if(clazz ! =null && clazz.getConstructors().length > 1) {
        // The test class cannot have a parameter constructor
        throw new IllegalArgumentException(
            "Test class can only have one constructor");
    }

    Map<Class<? extends Annotation>, List<FrameworkMethod>> methodsForAnnotations =
        new LinkedHashMap<Class<? extends Annotation>, List<FrameworkMethod>>();
    Map<Class<? extends Annotation>, List<FrameworkField>> fieldsForAnnotations =
        new LinkedHashMap<Class<? extends Annotation>, List<FrameworkField>>();
    // Scan all Junit annotations in the class to be tested, including @test@before@after, etc
    scanAnnotatedMembers(methodsForAnnotations, fieldsForAnnotations);
	// Filter out the comments typed on the method,
    this.methodsForAnnotations = makeDeeplyUnmodifiable(methodsForAnnotations);
    // Filter out comments typed on variables
    this.fieldsForAnnotations = makeDeeplyUnmodifiable(fieldsForAnnotations);
}
Copy the code

MethodsForAnnotations and fieldsForAnnotations cache all methods and variables marked by junit annotations for the class currently under test

  • 2. Select all @test annotation methods from methodsForAnnotations in getFilteredChildren. (getDescription()-> getFilteredChildren -> computeTestMethods -> filter from methodsForAnnotations by type)
  • Return all @test annotated methods

Before and After execution timing

To solve this problem, it is actually necessary to understand one of the more important concepts in Junit, Statement.

public abstract class Statement {
    /**
     * Run the action, throwing a {@code Throwable} if anything goes wrong.
     */
    public abstract void evaluate(a) throws Throwable;
}
Copy the code

Statement came out of junit 4.5. A Statement represents one or more operations to be performed at run time while running a junit test component. Simply put, for methods annotated with @before@After annotations, JUnit will exist as a Statement, corresponding to RunBefores and RunnerAfter, that holds all the frameworkmethods currently running.

The FrameworkMethod is the internal description of all the methods in JUnit that are annotated by JUnit. @test, @before, @After, @beforeClass, and @AfterClass annotated methods all end up as FrameworkMethod instances.

A Statement can be created in two ways: methodBlock based on the FrameworkMethod and classBlock based on the RunNotifier. MethodBlock is described here and classBlock is discussed in the following section.

protected Statement methodBlock(final FrameworkMethod method) {
        Object test;
        try {
            test = new ReflectiveCallable() {
                @Override
                protected Object runReflectiveCall(a) throws Throwable {
                    return createTest(method);
                }
            }.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);
        statement = withInterruptIsolation(statement);
        return statement;
    }
Copy the code

WithAfters and withBefores bind RunAfters and RunBefore to a statement, resulting in a statement chain whose execution entry is RunAfters#evaluate.

@Override
public void evaluate(a) throws Throwable {
    List<Throwable> errors = new ArrayList<Throwable>();
    try {
        next.evaluate();
    } catch (Throwable e) {
        errors.add(e);
    } finally {
        // Execute the after method in finally
        for (FrameworkMethod each : afters) {
            try {
                invokeMethod(each);
            } catch (Throwable e) {
                errors.add(e);
            }
        }
    }
    MultipleFailureException.assertEmpty(errors);
}
Copy the code

The next chain includes before and test methods to execute

So we see before -> testMethod -> after.

This is not exactly what you expect. The first idea of before and after logic is to proxy intercept test methods in a way similar to before and after in Spring AOP, but it is not.

BeforeClass and AfterClass Execution timing

Junit creates a statement using methodBlock and binds before and after methods to the statement. ClassBlock binds BeforeClass and AfterClass to statement.

protected Statement classBlock(final RunNotifier notifier) {
    // childrenInvoker this will call methodBlock
    Statement statement = childrenInvoker(notifier);
    if(! areAllChildrenIgnored()) { statement = withBeforeClasses(statement); statement = withAfterClasses(statement); statement = withClassRules(statement); statement = withInterruptIsolation(statement); }return statement;
}
Copy the code

BeforeClass and before both create RunnerBefores. The difference is that BeforeClass does not specify the target test method when creating RunnerBefores.

  • BeforeClass Runs all non-overridden @beforeClass methods on the class and superclass before statement is executed. If an exception is thrown, stop execution and pass the exception.
  • AfterClass runs all unoverridden @AfterClass methods on the AfterClass and superclass at the end of the Statement chain; Always perform all AfterClass methods: if necessary, to the exception thrown in front of the steps with abnormal from AfterClass method combined to org. Junit. Runners. Model. MultipleFailureException.

How does Junit collect execution results and return them

The results of all junit executions are stored in Result

// Count all cases
private final AtomicInteger count;
// Ignore the number of cases executed (labelled ignore)
private final AtomicInteger ignoreCount;
// Number of failed cases
private final AtomicInteger assumptionFailureCount;
// The result of all failed cases
private final CopyOnWriteArrayList<Failure> failures;
// Execution time
private final AtomicLong runTime;
// Start time
private final AtomicLong startTime;
Copy the code

Result has a built-in default Listener that calls back after each case is executed. The Listener is as follows:

@RunListener.ThreadSafe
    private class Listener extends RunListener {
    // Set the start time
        @Override
        public void testRunStarted(Description description) throws Exception {
            startTime.set(System.currentTimeMillis());
        }
		
        // Execute all cases
        @Override
        public void testRunFinished(Result result) throws Exception {
            long endTime = System.currentTimeMillis();
            runTime.addAndGet(endTime - startTime.get());
        }
		// Finish executing a case
        @Override
        public void testFinished(Description description) throws Exception {
            count.getAndIncrement();
        }
		// Failed to execute a case
        @Override
        public void testFailure(Failure failure) throws Exception {
            failures.add(failure);
        }
		// Execute an ignore case
        @Override
        public void testIgnored(Description description) throws Exception {
            ignoreCount.getAndIncrement();
        }

        @Override
        public void testAssumptionFailure(Failure failure) {
        // Assumption generated failureassumptionFailureCount.getAndIncrement(); }}Copy the code

JUnit 4 begins to support hypothesis Assumptions in testing, and within the Assumptions, encapsulates a set of methods used to support conditional test execution based on Assumptions. If the hypothesis is not met, the hypothesis does not cause the test to fail, but merely terminates the current test. This is also the biggest difference between assumptions and assertions, for which the test will fail.

So JUnit uses the listener mechanism to collect all the test information and finally returns it in Result.

conclusion

There are some basic concepts in Junit, such as Runner, statement, etc. At initialization, junit builds a Runner such as BlockJUnit4ClassRunner by default, and this Runner holds all the information about the class being tested. Runner runs the test and notifies RunNotifier of important events when doing so.

You can also use RunWith to call a custom Runner, as long as your Runner is a subclass of org.junit.runner. When you create a custom run program, in addition to implementing the abstract method here, you must also provide a constructor that takes the class containing the test as an argument — for example, SpringRunner.

Inside the Runner’s run method is the process of building and executing the Statement chain, which describes a series of operations to be performed in the unit test. RunnerAfter -> TargetMethod -> RunnerBefore; During execution, junit calls back each lifecycle stage of case invocation through the listener mechanism, collects and summarizes the execution information of each case, and finally returns the Result of execution.