This JUnit 5 tutorial talks about how JUnit adapted the Java 8 style of coding and several other new features of JUnit 5 library. Also, learn how JUnit 5 is different from JUnit 4.
JUnit is the most widely used testing framework for Java applications. For a very long time, JUnit has been doing its job perfectly.
In between, JDK 8 brought fascinating features in Java and, most notably, lambda expressions. JUnit 5 aimed to adapt the Java 8 style of coding; that’s why Java 8 is the minimum required version to create and execute tests in JUnit 5 (though it is possible to run tests written with JUnit 3 or JUnit 4 for backward compatibility).
1. JUnit 5 Architecture
As compared to JUnit 4, JUnit 5 is composed of several different modules from three different sub-projects:
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
- JUnit Jupiter: It includes new programming and extension models for writing tests. It has all new junit annotations and
TestEngine
implementation to run tests written with these annotations. - JUnit Platform: To be able to launch junit tests, IDEs, build tools or plugins need to include and extend platform APIs. It defines the
TestEngine
API for developing new testing frameworks that run on the platform.
It also provides a Console Launcher to launch the platform from the command line and build plugins for Gradle and Maven. - JUnit Vintage: Its primary purpose is to support running JUnit 3 and JUnit 4 written tests on the JUnit 5 platform. It’s there is backward compatibility. It requires JUnit 4.12 or later to be present on the class path or module path.
2. JUnit 5 Maven Dependencies
You can use JUnit 5 in your Maven or Gradle project by including a minimum of these dependencies:
- junit-jupiter-api: It is the main module where all core annotations are located, such as @Test, Lifecycle method annotations and assertions.
- junit-jupiter-engine: It has test engine implementation which is required at runtime to execute the tests.
- junit-platform-suite: The @Suite support provided by this module makes the JUnitPlatform runner obsolete.
<properties>
<junit.jupiter.version>5.8.1</junit.jupiter.version>
<junit.platform.version>1.8.1</junit.platform.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
dependencies {
testRuntime("org.junit.jupiter:junit-jupiter-api:5.8.1")
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.8.1")
testRuntime("org.junit.jupiter:junit-jupiter-params:5.8.1")
testRuntime("org.junit.platform:junit-platform-suite:1.8.1")
}
test {
useJUnitPlatform()
}
Read More: Maven Example | Gradle Example
3. JUnit 5 Annotations
3.1. Inbuilt Annotations
JUnit 5 offers the following inbuilt annotations to write tests.
A method that is directly annotated or meta-annotated with @BeforeAll
, @AfterAll
, @BeforeEach
, or @AfterEach
is called the lifecycle method.
Annotation | Description |
---|---|
@BeforeEach | The annotated method will be run before each test method in the test class. |
@AfterEach | The annotated method will be run after each test method in the test class. |
@BeforeAll | The annotated method will be run before all test methods in the test class. This method must be static. |
@AfterAll | The annotated method will be run after all test methods in the test class. This method must be static. |
@Test | It is used to mark a method as a junit test. |
@DisplayName | Used to provide any custom display name for a test class or test method |
@Disable | It is used to disable or ignore a test class or test method from the test suite. |
@Nested | Used to create nested test classes |
@Tag | Mark test methods or test classes with tags for test discovering and filtering |
@TestFactory | Mark a method as a test factory for dynamic tests. |
@ParameterizedTest | Denotes that a method is a parameterized test. |
@RepeatedTest | Denotes that a method is a test template for a repeated test. |
@TestClassOrder | Used to configure the test class execution order for @Nested test classes in the annotated test class. |
@TestMethodOrder | Used to configure the test method execution order for the annotated test class; similar to JUnit 4’s @FixMethodOrder. |
@Timeout | Used to fail a test, test factory, test template, or lifecycle method if its execution exceeds a given duration. |
@TempDir | Used to supply a temporary directory via field injection or parameter injection in a lifecycle method or test method. |
3.2. Composed Annotations
We can also create composed annotations that will automatically inherit the semantics of its meta-annotations.
For example, instead of copying and pasting @Tag(“developement”), we can create a composed annotation @Dev as follows:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("developement")
public @interface Dev {
}
And then we can use @Dev annotation in tests that have to be executed on the development environment. This makes it easy to change the environment name later, if needed, without modifying all the test classes.
@Dev
@Test
void someTest() {
// ...
}
4. Writing JUnit 5 Tests
There is not much change between JUnit 4 and JUnit 5 in test writing styles.
- A test class is any top-level class, static member class, or @Nested class that contains at least one test method. Test classes must not be
abstract
and must have a single constructor. - A test method is written using @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, or @TestTemplate annotations.
- Test classes, test methods, and lifecycle methods are not required to be public, but they must not be private. Using public modifier is recommended.
Here are sample tests with their life cycle methods. Notice all the annotations are coming from org.junit.jupiter.api package.
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import com.howtodoinjava.junit5.examples.Calculator;
public class AppTest {
@BeforeAll
static void setup(){
System.out.println("@BeforeAll executed");
}
@BeforeEach
void setupThis(){
System.out.println("@BeforeEach executed");
}
@Tag("DEV")
@Test
void testCalcOne() {
System.out.println("======TEST ONE EXECUTED=======");
Assertions.assertEquals( 4 , Calculator.add(2, 2));
}
@Tag("PROD")
@Disabled
@Test
void testCalcTwo(){
System.out.println("======TEST TWO EXECUTED=======");
Assertions.assertEquals( 6 , Calculator.add(2, 4));
}
@AfterEach
void tearThis(){
System.out.println("@AfterEach executed");
}
@AfterAll
static void tear(){
System.out.println("@AfterAll executed");
}
}
5. Writing Junit 5 Test Suites
Using JUnit 5 test suites, you can run tests spread into multiple test classes and different packages. JUnit 5 provides these annotations to create test suites.
- @Suite
- @SelectClasses
- @SelectPackages
- @IncludePackages
- @ExcludePackages
- @IncludeClassNamePatterns
- @ExcludeClassNamePatterns
- @IncludeTags
- @ExcludeTags
To execute the suite, you need to use @Suite
annotation and include junit-platform-suite module in the project dependencies.
@Suite
@SelectPackages("com.howtodoinjava.junit5.examples")
public class JUnit5TestSuiteExample {
//...
}
6. JUnit 5 Assertions
Assertions help in validating the expected output with the actual output of a test.
To keep things simple, all JUnit Jupiter assertions are static
methods in the org.junit.jupiter.Assertions class e.g. assertEquals()
, assertNotEquals()
.
void testCase() {
//Test will pass
Assertions.assertNotEquals(3, Calculator.add(2, 2));
//Test will fail
Assertions.assertNotEquals(4, Calculator.add(2, 2), "Calculator.add(2, 2) test failed");
//Test will fail
Supplier<String> messageSupplier = () -> "Calculator.add(2, 2) test failed";
Assertions.assertNotEquals(4, Calculator.add(2, 2), messageSupplier);
}
Read More: JUnit 5 Assertions
7. JUnit 5 Assumptions
Assumptions class provides static
methods to support conditional test execution based on assumptions. A failed assumption results in a test being aborted.
Assumptions are typically used whenever it does not make sense to continue the execution of a given test method. In the test report, these tests will be marked as passed.
The Assumptions class has three such methods: assumeFalse()
, assumeTrue()
and assumingThat()
public class AppTest {
@Test
void testOnDev()
{
System.setProperty("ENV", "DEV");
Assumptions.assumeTrue("DEV".equals(System.getProperty("ENV")), AppTest::message);
}
@Test
void testOnProd()
{
System.setProperty("ENV", "PROD");
Assumptions.assumeFalse("DEV".equals(System.getProperty("ENV")));
}
private static String message () {
return "TEST Execution Failed :: ";
}
}
Read More: JUnit 5 Assumptions
8. Backward Compatibility for JUnit 4
Since all classes and annotations specific to JUnit Jupiter reside under the org.junit.jupiter
base package, having both JUnit 4 and JUnit Jupiter in the classpath does not lead to any conflicts. Therefore, it is recommended to write the new tests on Junit 5 infrastructure.
JUnit 4 has been here for quite a long time, and there are numerous tests written in junit 4. JUnit Jupiter needs to support those tests as well. For this purpose, the JUnit Vintage sub-project was developed.
JUnit Vintage provides a TestEngine implementation for running JUnit 3 and JUnit 4-based tests on the JUnit 5 platform. As long as we have junit-vintage-engine artifact in the classpath, JUnit 3 and JUnit 4 tests will automatically be picked up by the JUnit Platform launcher.
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
9. Migration from JUnit 4 to JUnit 5
While JUnit 5 provides support for JUnit 4’s annotations, it’s recommended to migrate to the new annotations to take full advantage of JUnit 5 features.
The official JUnit 5 reference has a list of changes that we need to make for a robust migration.
If you have legacy code or external dependencies that rely heavily on JUnit 4, you might need to consider a more gradual migration strategy. You could start by running JUnit 4 and JUnit 5 tests side by side until you’re confident in migrating completely.
10. Conclusion
JUnit 5 feels so exciting and feature-rich. And now, it is open for extension by third-party tools and APIs. As a test writer, you may not feel that much different, but when you will go for its extension or try to develop an IDE plugin, you will praise it.
You may also consider adding test templates into eclipse IDE to improve your development speed as a developer.
Happy Learning !!