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

Onur Baskirt is a Software Engineering Leader with international experience in world-class companies. Now, he is a Software Engineering Lead at Emirates Airlines in Dubai.