1. Overview
In this tutorial, we'll look at the basic structure of a JUnit test rule.
2. TestRule Interface
The TestRule interface is the main interface we must implement to create a Junit test rule.
TestRule has only one method, apply. This method gets two parameters: a statement (Statement) and a description (Description).
public interface TestRule {
/**
* Modifies the method-running {@link Statement} to implement this
* test-running rule.
*
* @param base The {@link Statement} to be modified
* @param description A {@link Description} of the test implemented in {@code base}
* @return a new statement, which may be the same as {@code base},
* a wrapper around {@code base}, or a completely new Statement.
*/
Statement apply(Statement base, Description description);
}
Now, let's look at these parameters.
2.1. Base Statement
The base parameter represents the actual test method that we write.
To better understand how JUnit stores a test method as an object, we must examine the built-in implementations of Statement. Out of these, InvokeMethod can store the details of a test method and invoke it using reflection:
public abstract class Statement {
public abstract void evaluate() throws Throwable;
}
public class InvokeMethod extends Statement {
private final FrameworkMethod testMethod;
private final Object target;
public InvokeMethod(FrameworkMethod testMethod, Object target) {
this.testMethod = testMethod;
this.target = target;
}
@Override
public void evaluate() throws Throwable {
testMethod.invokeExplosively(target);
}
}
So, we're assuming that base in TestRule.apply is an instance of InvokeMethod. However, the base statement may not always be an instance of InvokeMethod since JUnit can also wrap it in other Statements. To further analyze, let's investigate the following snippet from BlockJUnit4ClassRunner:
protected Statement methodBlock(FrameworkMethod method) {
...
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;
}
It first creates an InvokeMethod instance. Then if there is an expected exception, it creates another statement. It then checks whether a timeout is set. In each phase, InvokeMethod gets wrapped in another statement. So our test rule gets the last created statement, not the original InvokeMethod instance.
Moreover, if we apply multiple test rules to a test, the result of one test rule will be the base statement of the next rule. We can see this behavior in the RunRules class:
public class RunRules extends Statement {
private final Statement statement;
public RunRules(Statement base, Iterable<TestRule> rules, Description description) {
statement = applyAll(base, rules, description);
}
@Override
public void evaluate() throws Throwable {
statement.evaluate();
}
private static Statement applyAll(Statement result, Iterable<TestRule> rules,
Description description) {
for (TestRule each : rules) {
result = each.apply(result, description);
}
return result;
}
}
2.2. Description
The description parameter provides information about the test. We can get the declared annotations, parent test class, and other information.
3. How to Handle Original Statement
Now let's look at how we can handle the original statement in our test rule.
The general idea is that we get the base statement and return a new statement. This new statement may be a wrapper around the original statement or it may be a brand new statement. Moreover, we can put checks before the original statement and skip the test if the checks fail.
Next, we'll investigate some built-in implementations.
3.1. Details of Verifier
The Verifier base class provides a template so that we can perform verification checks after the test completes.
public abstract class Verifier implements TestRule {
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
verify();
}
};
}
/**
* Override this to add verification logic. Overrides should throw an
* exception to indicate that verification failed.
*/
protected void verify() throws Throwable {
}
}
Note that the returned statement is a wrapper around the original one.
3.2. Details of ExternalResource
Let's continue with ExternalResource.
It also provides a template so that we can open and close an external resource.
Similar to Verifier, ExternalResource returns a wrapper statement around the original one.
public abstract class ExternalResource implements TestRule {
public Statement apply(Statement base, Description description) {
return statement(base);
}
private Statement statement(final Statement base) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
before();
try {
base.evaluate();
} finally {
after();
}
}
};
}
/**
* Override to set up your specific external resource.
*
* @throws Throwable if setup fails (which will disable {@code after}
*/
protected void before() throws Throwable {
// do nothing
}
/**
* Override to tear down your specific external resource.
*/
protected void after() {
// do nothing
}
}
4. Summary
In this tutorial, we've looked at the internal structure of a JUnit test rule.
We can think of the TestRule implementations as decorators around our test code. After all test rules are applied, the final Statement can have different layers of functionality.