JUnit 5 Extensions

Junit 5 Extensions makes the Junit 5 much more powerful and extensible. In this article, we will learn JUnit Extensions.  By using JUnit Extensions we will enhance and extend JUnit capabilities. By extending the behavior of our tests classes and methods, we can use the extended behavior in our test project. In JUnit 4, we can do these operations by using JUnit Rules.

Junit 5 Extensions Overview

In order to use JUnit extensions, we need to register them. When we register extensions, JUnit engine calls them at certain points or in other words extension points during the test execution. We can use below extension points in our test:

  • life-cycle callbacks
  • test instance post-processing
  • conditional test execution
  • exception handling
  • parameter resolution

To use extensions in our projects, we need to create Extension classes and register them to our tests. JUnit extension implements interfaces corresponding to JUnit extension points.

Lifecycle Callbacks and Test Instance Post-Processing Callback

We need to know how and when the extensions of JUnit 5 are invoked in our tests. If we have some uncertainty for this, we may face with unpredicted results. 

Normally, by default JUnit 5 provides us below annotations to manage test life cycle in our tests.

package tests;

import org.junit.jupiter.api.*;

public class DefaultTestLifeCycleTest {

    @BeforeAll
    static void beforeAll() {
        System.out.println("beforeAll()");
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("beforeEach()");
    }

    @Test
    void firstTest() {
        System.out.println("firstTest()");
    }

    @Test
    void secondTest() {
        System.out.println("secondTest()");
    }

    @AfterEach
    void afterEach() {
        System.out.println("afterEach()");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("afterAll()");
    }
}

This execution will print below output. This is an expected behavior.

beforeAll()
beforeEach()
firstTest()
afterEach()
beforeEach()
secondTest()
afterEach()
afterAll()

Now. let’s write our test life cycle extension test. First, we need to write our extension class. A sample test life cycle extension class is as follows. We need to implement all callback classes and also TestInstancePostProcessor class.

package extensions;

import org.junit.jupiter.api.extension.*;

public class TestLifeCycleExtensions implements TestInstancePostProcessor, BeforeAllCallback, BeforeEachCallback,
        BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterEachCallback, AfterAllCallback {

    @Override
    public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
        log("TestInstancePostProcessor");
    }

    @Override
    public void beforeAll(ExtensionContext context) {
        log("BeforeAllCallback");
    }

    @Override
    public void beforeEach(ExtensionContext context) {
        log("BeforeEachCallback");
    }

    @Override
    public void beforeTestExecution(ExtensionContext context) {
        log("BeforeTestExecutionCallback");
    }

    @Override
    public void afterTestExecution(ExtensionContext context) {
        log("AfterTestExecutionCallback");
    }

    @Override
    public void afterEach(ExtensionContext context) {
        log("AfterEachCallback");
    }

    @Override
    public void afterAll(ExtensionContext context) {
        log("AfterAllCallback");
    }

    private void log(String logText) {
        System.out.println(logText);
    }
}

In order to register our extension class to our test classes, we need to use class level @ExtentWith annotation. You can see this below test class. The code is the same but this time, we registered our extension class with @ExtentWith annotation.

package tests;

import extensions.TestLifeCycleExtensions;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(TestLifeCycleExtensions.class)
public class TestLifeCycleExtensionTest {
    @BeforeAll
    static void beforeAll() {
        System.out.println("beforeAll()");
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("beforeEach()");
    }

    @Test
    void firstTest() {
        System.out.println("firstTest()");
    }

    @Test
    void secondTest() {
        System.out.println("secondTest()");
    }

    @AfterEach
    void afterEach() {
        System.out.println("afterEach()");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("afterAll()");
    }
}

And this time test result will be like that.

BeforeAllCallback
beforeAll()
TestInstancePostProcessor
BeforeEachCallback
beforeEach()
BeforeTestExecutionCallback
firstTest()
AfterTestExecutionCallback
afterEach()
AfterEachCallback
TestInstancePostProcessor
BeforeEachCallback
beforeEach()
BeforeTestExecutionCallback
secondTest()
AfterTestExecutionCallback
afterEach()
AfterEachCallback
afterAll()
AfterAllCallback

As you see above, we can use test lifecycle extensions as Listeners like TestNG Listeners. At each point, you can manipulate your test code with these extensions. Also, BeforeAllCallback and AfterAllCallback are like TestNG‘s @BeforeSuite and @AfterSuite annotations. This is a missing part of the JUnit and now you can use this with JUnit extension models.

Conditional Test Execution

With this extension, we can control the test executions. In order to do this, we need to implement ExecutionCondition interface. We can disable and enable our tests for a specific environment with this extension. For example, let’s set our environment as “development” in our configuration properties file and then implement ExecutionCondition interface in our conditional extension class and write a logic that does not run our tests in the development environment. Finally, we should register this extension with @ExtentWith annotation on top of our test class.

package extensions;

import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class ExecutionConditionExtension implements ExecutionCondition {

    private static String propertyFilePath = System.getProperty("user.dir")+
            "/src/test/java/resources/junit-platform.properties";

    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context)  {

        Properties prop = new Properties();
        try {
            prop.load(new FileInputStream(propertyFilePath));
        } catch (IOException e) {
            e.printStackTrace();
        }

        String environment = prop.getProperty("environment");
        if (environment.equals("development")) {
            return ConditionEvaluationResult.disabled("Test disabled on Development environment.");
        }

        return ConditionEvaluationResult.enabled(
                "Test enabled on Test environment");
    }
}

Let’s register this extension.

package tests;

import extensions.ExecutionConditionExtension;
import extensions.TestLifeCycleExtension;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(TestLifeCycleExtension.class)
@ExtendWith(ExecutionConditionExtension.class)
public class TestLifeCycleExtensionTest {
    @BeforeAll
    static void beforeAll() {
        System.out.println("beforeAll()");
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("beforeEach()");
    }

    @Test
    void firstTest() {
        System.out.println("firstTest()");
    }

    @Test
    void secondTest() {
        System.out.println("secondTest()");
    }

    @AfterEach
    void afterEach() {
        System.out.println("afterEach()");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("afterAll()");
    }
}

And this is our properties file.

environment=development

When you run the test, you will see the results as follows.

"C:\Program Files\Java\jdk1.8.0_172\bin\java.exe" -ea -Didea.test.cyclic.buffer.size=1048576 "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2018.2.1\lib\idea_rt.jar=51811:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2018.2.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2018.2.1\lib\idea_rt.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2018.2.1\plugins\junit\lib\junit-rt.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2018.2.1\plugins\junit\lib\junit5-rt.jar;C:\Users\onur\.m2\repository\org\junit\vintage\junit-vintage-engine\5.3.1\junit-vintage-engine-5.3.1.jar;C:\Users\onur\.m2\repository\org\apiguardian\apiguardian-api\1.0.0\apiguardian-api-1.0.0.jar;C:\Users\onur\.m2\repository\org\junit\platform\junit-platform-engine\1.3.1\junit-platform-engine-1.3.1.jar;C:\Users\onur\.m2\repository\org\junit\platform\junit-platform-commons\1.3.1\junit-platform-commons-1.3.1.jar;C:\Users\onur\.m2\repository\org\opentest4j\opentest4j\1.1.1\opentest4j-1.1.1.jar;C:\Users\onur\.m2\repository\junit\junit\4.12\junit-4.12.jar;C:\Users\onur\.m2\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar;C:\Users\onur\Google Drive\PROJECTS\MyOwnProjects\Selenium-Training\Selenium-Basics\Projects\Junit5Extensions\target\test-classes;C:\Users\onur\Google Drive\PROJECTS\MyOwnProjects\Selenium-Training\Selenium-Basics\Projects\Junit5Extensions\target\classes;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-java\3.14.0\selenium-java-3.14.0.jar;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-api\3.14.0\selenium-api-3.14.0.jar;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-chrome-driver\3.14.0\selenium-chrome-driver-3.14.0.jar;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-edge-driver\3.14.0\selenium-edge-driver-3.14.0.jar;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-firefox-driver\3.14.0\selenium-firefox-driver-3.14.0.jar;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-ie-driver\3.14.0\selenium-ie-driver-3.14.0.jar;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-opera-driver\3.14.0\selenium-opera-driver-3.14.0.jar;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-remote-driver\3.14.0\selenium-remote-driver-3.14.0.jar;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-safari-driver\3.14.0\selenium-safari-driver-3.14.0.jar;C:\Users\onur\.m2\repository\org\seleniumhq\selenium\selenium-support\3.14.0\selenium-support-3.14.0.jar;C:\Users\onur\.m2\repository\net\bytebuddy\byte-buddy\1.8.15\byte-buddy-1.8.15.jar;C:\Users\onur\.m2\repository\org\apache\commons\commons-exec\1.3\commons-exec-1.3.jar;C:\Users\onur\.m2\repository\commons-codec\commons-codec\1.10\commons-codec-1.10.jar;C:\Users\onur\.m2\repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;C:\Users\onur\.m2\repository\com\google\guava\guava\25.0-jre\guava-25.0-jre.jar;C:\Users\onur\.m2\repository\com\google\code\findbugs\jsr305\1.3.9\jsr305-1.3.9.jar;C:\Users\onur\.m2\repository\org\checkerframework\checker-compat-qual\2.0.0\checker-compat-qual-2.0.0.jar;C:\Users\onur\.m2\repository\com\google\errorprone\error_prone_annotations\2.1.3\error_prone_annotations-2.1.3.jar;C:\Users\onur\.m2\repository\com\google\j2objc\j2objc-annotations\1.1\j2objc-annotations-1.1.jar;C:\Users\onur\.m2\repository\org\codehaus\mojo\animal-sniffer-annotations\1.14\animal-sniffer-annotations-1.14.jar;C:\Users\onur\.m2\repository\org\apache\httpcomponents\httpclient\4.5.5\httpclient-4.5.5.jar;C:\Users\onur\.m2\repository\org\apache\httpcomponents\httpcore\4.4.9\httpcore-4.4.9.jar;C:\Users\onur\.m2\repository\com\squareup\okhttp3\okhttp\3.10.0\okhttp-3.10.0.jar;C:\Users\onur\.m2\repository\com\squareup\okio\okio\1.14.1\okio-1.14.1.jar;C:\Users\onur\.m2\repository\org\junit\jupiter\junit-jupiter-api\5.3.1\junit-jupiter-api-5.3.1.jar;C:\Users\onur\.m2\repository\org\junit\jupiter\junit-jupiter-engine\5.3.1\junit-jupiter-engine-5.3.1.jar;C:\Users\onur\.m2\repository\org\junit\platform\junit-platform-launcher\1.3.1\junit-platform-launcher-1.3.1.jar;C:\Users\onur\.m2\repository\io\github\artsok\rerunner-jupiter\1.1.1\rerunner-jupiter-1.1.1.jar;C:\Users\onur\.m2\repository\org\junit\platform\junit-platform-runner\1.0.0\junit-platform-runner-1.0.0.jar;C:\Users\onur\.m2\repository\org\junit\platform\junit-platform-suite-api\1.0.0\junit-platform-suite-api-1.0.0.jar;C:\Users\onur\.m2\repository\org\projectlombok\lombok\1.16.16\lombok-1.16.16.jar;C:\Users\onur\.m2\repository\org\slf4j\slf4j-simple\1.7.5\slf4j-simple-1.7.5.jar;C:\Users\onur\.m2\repository\org\slf4j\slf4j-api\1.7.5\slf4j-api-1.7.5.jar" com.intellij.rt.execution.junit.JUnitStarter -ideVersion5 -junit5 tests.TestLifeCycleExtensionTest
Nov 05, 2018 11:48:08 PM org.junit.platform.launcher.core.LauncherConfigurationParameters fromClasspathResource
INFO: Loading JUnit Platform configuration parameters from classpath resource [file:.../Junit5Extensions/target/test-classes/junit-platform.properties].

Test disabled on Development environment.

Test disabled on Development environment.

Process finished with exit code 0

Parameter Resolution Extension

In some situations, we need to define parameters for test methods and test constructors. In these kinds of cases, we should use ParameterResolver class to resolve the parameters dynamically at runtime. @Test, @BeforeEach, @AfterEach, @BeforeAll, or @AfterAll method accepts a parameter and this parameter can be resolved by name, annotation, type, etc.

However, if a parameter is defined in JUnit 5 library by default, we will not face any problem. For example, TestInfo is a class defined in JUnit 5 and it comprises information about the current test.

@Test
void testSomething(TestInfo info){
     //Your JUnit test
}

If our test has a parameter as FraudService, it has to be resolved at runtime. Thus, we need to use ParameterResolver extension point.

FraudServiceParameterResolver class will be like that:

public class FraudServiceParameterResolver implements ParameterResolver {
  @Override
  public Object resolveParameter(ParameterContext parameterContext,
      ExtensionContext extensionContext) throws ParameterResolutionException {
      return new FraudService();
  }
 
  @Override
  public boolean supportsParameter(ParameterContext parameterContext,
      ExtensionContext extensionContext) throws ParameterResolutionException {
      return (parameterContext.getParameter().getType() == FraudService.class);
  }
}

In the FraudServiceParameterResolver class, we need to implement  ParameterResolver class. In the supportParameter method, we check the parameter type is as FraudService and the resolveParameter method, we return the new instance of FraudService. In order to use this extension, we should register this with @ExtentWith(FraudServiceParameterResolver.class) annotation on top of our test classes or BaseTest class.

Exception Handling Extension

For specific types of exceptions, we need to implement TestExecutionExceptionHandler interface. The below example demonstrates an ignoring StaleElementReference extension which ignores all StaleElementReferenceException but rethrows other exceptions.

public class IgnoreStaleElementReferenceExceptionExtension implements TestExecutionExceptionHandler {
    @Override
    public void handleTestExecutionException(ExtensionContext context, Throwable throwable)
            throws Throwable {
        if (throwable instanceof StaleElementReferenceException) {
            return;
        }
        throw throwable;
    }
}

In this article, I tried to explain how to create custom JUnit 5 extensions. You can find sample project for Junit 5 Extensions on swtestacademy GitHub page.

Thanks.
Onur Baskirt

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.