Junit4 architecture design series: Runner.run() and Statement

Keywords: Junit REST

Overall##

Series entrance:

Junit4 architecture design series (1): Request, ClassRequest and runner builder

In the previous article, we have basically sorted out the Flow of Junit4 execution Case in general:
Request -> ClassRequest.getRunner() -> AllDefaultPossibilitiesBuilder.safeRunnerForClass() -> runner.run()

And introduced the classes Request, ClassRequest, and runner builder, the rest runner.run() I didn't say that, so this article starts from here.

Runner.run How does () execute Case? To talk about

The run() method is a method defined by the abstract class Runner to execute Case. Each subclass of Runner needs to implement this method.

 /**
 * Run the tests for this runner.
 *
 * @param notifier will be notified of events while tests are being run--tests being
 * started, finishing, and failing
 */
public abstract void run(RunNotifier notifier);

As we know before, the Runner responsible for executing Junit4 style case by default is BlockJUnit4ClassRunner, but BlockJUnit4ClassRunner does not directly inherit the Runner class, but there is an additional layer of parentrunner < T > in the middle, as shown below:

ParentRunner class is responsible for filter and sort Test Class, processing @ BeforeClass and @AfterClass methods, and various ClassRules, and executing Test Class in order.

Let's see how the ParentRunner implements Junit4 style Test Class. The specific implementation is as follows:

@Override
public void run(final RunNotifier notifier) {
    EachTestNotifier testNotifier = new EachTestNotifier(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);
    }
}

The first line is to instantiate an EachTestNotifier, mainly to record the execution process, which will not be explained in detail here.
Obviously, the logic in Try is the real execution step. Logic is also very clear, that is, get Statement, and then call the evaluate() method.

Continue to follow the method classBlock, and we will see the following logic:

protected Statement classBlock(final RunNotifier notifier) {
    Statement statement = childrenInvoker(notifier);
    if (!areAllChildrenIgnored()) {
        statement = withBeforeClasses(statement);
        statement = withAfterClasses(statement);
        statement = withClassRules(statement);
    }
    return statement;
}

This logic is used to combine the statements we expect, Runner.run() method mainly involves the following execution logic:

  • The childInvoker method indicates to execute this Test Class
  • With beforeclasses is to judge whether the Test Class is decorated by @ BeforeClass? If there's any, we need to implement it first
  • With afterclasses, judge whether the Test Class has a @ AfterClass decorated method
  • With classrules this is to check whether the Test Class is applicable to TestRules

When all statements are wrapped, call statement.evaluate() we can execute the desired results in order as required.

See this kind of nested input and output writing method, will you have a sudden feeling?! This is the classic scene of decorator mode.

It should be noted that Class is handled here, and for Method level, there are also applicable scenarios similar to decorator mode. We will follow childInvoker from above, and finally we will find that BlockJunit4ClassRunner Class is the real implementation of Test Method. First, it implements the abstract Method runChild of ParentRunner:

//
// Implementation of ParentRunner
//

@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
    Description description = describeChild(method);
    if (isIgnored(method)) {
        notifier.fireTestIgnored(description);
    } else {
        Statement statement;
        try {
            statement = methodBlock(method);
        }
        catch (Throwable ex) {
            statement = new Fail(ex);
        }
        runLeaf(statement, description, notifier);
    }
}

Then, in the period method block, methodBlock combines statement:

protected Statement methodBlock(final FrameworkMethod method) {
    Object test;
    try {
        test = new ReflectiveCallable() {
            @Override
            protected Object runReflectiveCall() 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);
    return statement;
}

Similar to ClassBlock.

Detailed Statement##

Statement is a very important part of Junit4. It can be written in a big way, as shown in the following figure:

In JUnit 4, all actions can be represented by Statement. From the allocation method of the package above, we can see that JUnit 4 not only predefines some actions that must be used or are frequently used, such as

  • InvokeMethod execute Test Method
  • Runbefore executes @ Before method Before InvokeMethod
  • RunAfters executes the @ After method
  • Fail Throw Errors
  • FailOnTime set the entry for Timeout
  • ExpectException defines the exception expected to be thrown

It also defines RunRules, which makes it convenient for us to Reuse or redefine our own rules

Design pattern knowledge supplement##

Decorator mode###

Decorator Pattern dynamically adds responsibility to objects. In terms of extended functionality, the decorator pattern provides a more flexible alternative to inheritance.
This is the class diagram of the standard decorator pattern I implemented:


The characteristics of decorator mode are as follows:

  • Decorator and decorated object have the same supertype
  • We can use multiple decorators to package an object
  • The decorator can act on his / her own before / or after the behavior of the entrusted decorator to achieve a specific purpose
  • Objects can be decorated with decorators dynamically and indefinitely at runtime

The third highlighted point is very important. We know that Junit4 makes use of this point to enable @ BeforeClass and @ AfterClass and other annotations to execute as required or before or after.

Please wait##

Follow up plan

  • Junit4 architecture design series (3) RunNotifer

Children's shoes, if you think this article is still attentive and useful, why don't you like it (⊙ o ⊙)?

Posted by Online Connect on Sat, 23 May 2020 23:13:32 -0700