This tutorial explains the Junit 5’s most common annotations with examples.
JUnit 5 is the next generation of JUnit. The goal is to create an up-to-date foundation for developer-side testing on the JVM. This includes focusing on Java 8 and above, as well as enabling many different styles of testing.
- What is Junit 5
- Junit5 Features
- 1. Meta-Annotations and Composed Annotations
- 2. Display Names
- 3. Disabling Tests
- 4. Conditional Test Execution
- 5. Tagging and Filtering
- 6. Test Execution Order
- 7. Nested Tests
- 8. Repeated Test Examples
- 9. Parameterized Tests
- 10. Sources of Arguments
- 11. Customizing Display Names
- 12. Dynamic Tests
- 13. Lazy Initialization
- 14. Multiple assertions at once
- 15. Assumptions
- Migration from Junit 4
What is Junit 5
Unlike previous versions of JUnit, JUnit 5 is composed of several different modules from three different sub-projects.
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: The JUnit Platform serves as a foundation for launching testing frameworks on the JVM. It also defines the TestEngine API for developing a testing framework that runs on the platform. Furthermore, the platform provides a Console Launcher to launch the platform from the command line and a JUnit 4 based Runner for running any TestEngine on the platform in a JUnit 4 based environment. First-class support for the JUnit Platform also exists in popular IDEs (see IntelliJ IDEA, Eclipse, NetBeans, and Visual Studio Code) and build tools (see Gradle, Maven, and Ant).
JUnit Jupiter: JUnit Jupiter is the combination of the new programming model and extension model for writing tests and extensions in JUnit 5. The Jupiter sub-project provides a TestEngine for running Jupiter based tests on the platform.
JUnit Vintage: It provides a TestEngine for running JUnit 3 and JUnit 4 based tests on the platform.
Junit5 Features
1. Meta-Annotations and Composed Annotations
JUnit Jupiter annotations can be used as meta-annotations. That means that you can define your own composed annotationthat will automatically inherit the semantics of its meta-annotations.
For example, instead of copying and pasting @Tag("fast")
throughout your code base (see Tagging and Filtering), you can create a custom composed annotation named @Fast
as follows. @Fast
can then be used as a drop-in replacement for @Tag("fast")
.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Tag("fast") @Test public @interface FastTest { }
JUnit automatically recognizes the following as a @Test
method that is tagged with “fast”.
@FastTest void myFastTest() { // ... }
2. Display Names
Test classes and test methods can declare custom display names via @DisplayName
— with spaces, special characters, and even emojis — that will be displayed in test reports and by test runners and IDEs.
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @DisplayName("A special test case") class DisplayNameDemo { @Test @DisplayName("Custom test name containing spaces") void testWithDisplayNameContainingSpaces() { } @Test @DisplayName("╯°□°)╯") void testWithDisplayNameContainingSpecialCharacters() { } @Test @DisplayName("😱") void testWithDisplayNameContainingEmoji() { } }
3. Disabling Tests
Entire test classes or individual test methods may be disabled via the @Disabled
annotation, via one of the annotations discussed in Conditional Test Execution, or via a custom ExecutionCondition
.
Here’s a @Disabled
test class.
@Disabled("Disabled until bug #99 has been fixed") class DisabledClassDemo { @Test void testWillBeSkipped() { } }
class DisabledTestsDemo { @Disabled("Disabled until bug #42 has been resolved") @Test void testWillBeSkipped() { } }
4. Conditional Test Execution
The ExecutionCondition
extension API in JUnit Jupiter allows developers to either enable or disable a container or test based on certain conditions programmatically. The simplest example of such a condition is the built-inDisabledCondition
which supports the @Disabled
annotation (see Disabling Tests). In addition to @Disabled
, JUnit Jupiter also supports several other annotation-based conditions in the org.junit.jupiter.api.condition
package that allow developers to enable or disable containers and tests declaratively. See the following sections for details.
4.1 Operating System Conditions
@Test @EnabledOnOs({ LINUX, MAC }) void onLinuxOrMac() { // ... } @Test @DisabledOnOs(WINDOWS) void notOnWindows() { // ... }
4.2 Java Runtime Environment Conditions
@Test @EnabledOnJre({ JAVA_9, JAVA_10 }) void onJava9Or10() { // ... } @Test @DisabledOnJre(JAVA_9) void notOnJava9() { // ... }
4.3 System Property Conditions
@Test @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*") void onlyOn64BitArchitectures() { // ... } @Test @DisabledIfSystemProperty(named = "ci-server", matches = "true") void notOnCiServer() { // ... }
4.4 Environment Variable Conditions
@Test @EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server") void onlyOnStagingServer() { // ... } @Test @DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*") void notOnDeveloperWorkstation() { // ... }
5.5 Script-based Conditions
JUnit Jupiter provides the ability to either enable or disable a container or test depending on the evaluation of a script configured via the @EnabledIf
or @DisabledIf
annotation. Scripts can be written in JavaScript, Groovy, or any other scripting language for which there is support for the Java Scripting API, defined by JSR 223.
@Test // Static JavaScript expression. @EnabledIf("2 * 3 == 6") void willBeExecuted() { // ... } @RepeatedTest(10) // Dynamic JavaScript expression. @DisabledIf("Math.random() < 0.314159") void mightNotBeExecuted() { // ... } @Test // Regular expression testing bound system property. @DisabledIf("/32/.test(systemProperty.get('os.arch'))") void disabledOn32BitArchitectures() { assertFalse(System.getProperty("os.arch").contains("32")); } @Test @EnabledIf("'CI' == systemEnvironment.get('ENV')") void onlyOnCiServer() { assertTrue("CI".equals(System.getenv("ENV"))); } @Test // Multi-line script, custom engine name and custom reason. @EnabledIf(value = { "load('nashorn:mozilla_compat.js')", "importPackage(java.time)", "", "var today = LocalDate.now()", "var tomorrow = today.plusDays(1)", "tomorrow.isAfter(today)" }, engine = "nashorn", reason = "Self-fulfilling: {result}") void theDayAfterTomorrow() { LocalDate today = LocalDate.now(); LocalDate tomorrow = today.plusDays(1); assertTrue(tomorrow.isAfter(today)); }
5. Tagging and Filtering
Test classes and methods can be tagged via the @Tag
annotation. Those tags can later be used to filter test discovery and execution.
@Tag("fast") @Tag("model") class TaggingDemo { @Test @Tag("taxes") void testingTaxCalculation() { } }
6. Test Execution Order
By default, test methods will be ordered using an algorithm that is deterministic but intentionally nonobvious. This ensures that subsequent runs of a test suite execute test methods in the same order, thereby allowing for repeatable builds.
To control the order in which test methods are executed, annotate your test class or test interface with @TestMethodOrder
and specify the desired MethodOrderer
implementation.
@TestMethodOrder(OrderAnnotation.class) class OrderedTestsDemo { @Test @Order(1) void nullValues() { // perform assertions against null values } @Test @Order(2) void emptyValues() { // perform assertions against empty values } @Test @Order(3) void validValues() { // perform assertions against valid values } }
7. Nested Tests
@Nested
tests give the test writer more capabilities to express the relationship among several groups of tests. Here’s an elaborate example.
@DisplayName("A stack") class TestingAStackDemo { Stack<Object> stack; @Test @DisplayName("is instantiated with new Stack()") void isInstantiatedWithNew() { new Stack<>(); } @Nested @DisplayName("when new") class WhenNew { @BeforeEach void createNewStack() { stack = new Stack<>(); } @Test @DisplayName("is empty") void isEmpty() { assertTrue(stack.isEmpty()); } @Test @DisplayName("throws EmptyStackException when popped") void throwsExceptionWhenPopped() { assertThrows(EmptyStackException.class, stack::pop); } @Test @DisplayName("throws EmptyStackException when peeked") void throwsExceptionWhenPeeked() { assertThrows(EmptyStackException.class, stack::peek); } @Nested @DisplayName("after pushing an element") class AfterPushing { String anElement = "an element"; @BeforeEach void pushAnElement() { stack.push(anElement); } @Test @DisplayName("it is no longer empty") void isNotEmpty() { assertFalse(stack.isEmpty()); } @Test @DisplayName("returns the element when popped and is empty") void returnElementWhenPopped() { assertEquals(anElement, stack.pop()); assertTrue(stack.isEmpty()); } @Test @DisplayName("returns the element when peeked but remains not empty") void returnElementWhenPeeked() { assertEquals(anElement, stack.peek()); assertFalse(stack.isEmpty()); } } } }
8. Repeated Test Examples
The repeatedTest()
method demonstrates how to have an instance of RepetitionInfo
injected into a test to access the total number of repetitions for the current repeated test.
The next two methods demonstrate how to include a custom @DisplayName
for the @RepeatedTest
method in the display name of each repetition. customDisplayName()
combines a custom display name with a custom pattern and then uses TestInfo
to verify the format of the generated display name. Repeat!
is the {displayName}
which comes from the @DisplayName
declaration, and 1/1
comes from {currentRepetition}/{totalRepetitions}
. In contrast,customDisplayNameWithLongPattern()
uses the aforementioned predefined RepeatedTest.LONG_DISPLAY_NAME
pattern.
repeatedTestInGerman()
demonstrates the ability to translate display names of repeated tests into foreign languages — in this case German, resulting in names for individual repetitions such as: Wiederholung 1 von 5
, Wiederholung 2 von 5
, etc.
class RepeatedTestsDemo { private Logger logger = // ... @BeforeEach void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) { int currentRepetition = repetitionInfo.getCurrentRepetition(); int totalRepetitions = repetitionInfo.getTotalRepetitions(); String methodName = testInfo.getTestMethod().get().getName(); logger.info(String.format("About to execute repetition %d of %d for %s", // currentRepetition, totalRepetitions, methodName)); } @RepeatedTest(10) void repeatedTest() { // ... } @RepeatedTest(5) void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) { assertEquals(5, repetitionInfo.getTotalRepetitions()); } @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}") @DisplayName("Repeat!") void customDisplayName(TestInfo testInfo) { assertEquals("Repeat! 1/1", testInfo.getDisplayName()); } @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME) @DisplayName("Details...") void customDisplayNameWithLongPattern(TestInfo testInfo) { assertEquals("Details... :: repetition 1 of 1", testInfo.getDisplayName()); } @RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}") void repeatedTestInGerman() { // ... } }
Output:
INFO: About to execute repetition 1 of 10 for repeatedTest INFO: About to execute repetition 2 of 10 for repeatedTest INFO: About to execute repetition 3 of 10 for repeatedTest INFO: About to execute repetition 4 of 10 for repeatedTest INFO: About to execute repetition 5 of 10 for repeatedTest INFO: About to execute repetition 6 of 10 for repeatedTest INFO: About to execute repetition 7 of 10 for repeatedTest INFO: About to execute repetition 8 of 10 for repeatedTest INFO: About to execute repetition 9 of 10 for repeatedTest INFO: About to execute repetition 10 of 10 for repeatedTest INFO: About to execute repetition 1 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 2 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 3 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 4 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 5 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 1 of 1 for customDisplayName INFO: About to execute repetition 1 of 1 for customDisplayNameWithLongPattern INFO: About to execute repetition 1 of 5 for repeatedTestInGerman INFO: About to execute repetition 2 of 5 for repeatedTestInGerman INFO: About to execute repetition 3 of 5 for repeatedTestInGerman INFO: About to execute repetition 4 of 5 for repeatedTestInGerman INFO: About to execute repetition 5 of 5 for repeatedTestInGerman
When using the ConsoleLauncher
with the unicode theme enabled, execution of RepeatedTestsDemo
results in the following output to the console.
├─ RepeatedTestsDemo ✔ │ ├─ repeatedTest() ✔ │ │ ├─ repetition 1 of 10 ✔ │ │ ├─ repetition 2 of 10 ✔ │ │ ├─ repetition 3 of 10 ✔ │ │ ├─ repetition 4 of 10 ✔ │ │ ├─ repetition 5 of 10 ✔ │ │ ├─ repetition 6 of 10 ✔ │ │ ├─ repetition 7 of 10 ✔ │ │ ├─ repetition 8 of 10 ✔ │ │ ├─ repetition 9 of 10 ✔ │ │ └─ repetition 10 of 10 ✔ │ ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔ │ │ ├─ repetition 1 of 5 ✔ │ │ ├─ repetition 2 of 5 ✔ │ │ ├─ repetition 3 of 5 ✔ │ │ ├─ repetition 4 of 5 ✔ │ │ └─ repetition 5 of 5 ✔ │ ├─ Repeat! ✔ │ │ └─ Repeat! 1/1 ✔ │ ├─ Details... ✔ │ │ └─ Details... :: repetition 1 of 1 ✔ │ └─ repeatedTestInGerman() ✔ │ ├─ Wiederholung 1 von 5 ✔ │ ├─ Wiederholung 2 von 5 ✔ │ ├─ Wiederholung 3 von 5 ✔ │ ├─ Wiederholung 4 von 5 ✔ │ └─ Wiederholung 5 von 5 ✔
9. Parameterized Tests
Parameterized tests make it possible to run a test multiple times with different arguments. They are declared just like regular @Test
methods but use the @ParameterizedTest
annotation instead. In addition, you must declare at least onesource that will provide the arguments for each invocation and then consume the arguments in the test method.
The following example demonstrates a parameterized test that uses the @ValueSource
annotation to specify a String
array as the source of arguments.
@ParameterizedTest @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void palindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); }
When executing the above parameterized test method, each invocation will be reported separately. For instance, the ConsoleLauncher
will print output similar to the following.
palindromes(String) ✔ ├─ [1] racecar ✔ ├─ [2] radar ✔ └─ [3] able was I ere I saw elba ✔
10. Sources of Arguments
Out of the box, JUnit Jupiter provides quite a few source annotations. Each of the following subsections provides a brief overview and an example for each of them. Please refer to the Javadoc in the org.junit.jupiter.params.provider
package for additional information.
10. 1 ValueSource
@ValueSource
is one of the simplest possible sources. It lets you specify a single array of literal values and can only be used for providing a single argument per parameterized test invocation.
For example, the following @ParameterizedTest
method will be invoked three times, with the values 1
, 2
, and 3
respectively.
@ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) void testWithValueSource(int argument) { assertTrue(argument > 0 && argument < 4); }
10.2 Null and Empty Sources
In order to check corner cases and verify proper behavior of our software when it is supplied bad input, it can be useful to have null
and empty values supplied to our parameterized tests. The following annotations serve as sources of null
and empty values for parameterized tests that accept a single argument.
@NullSource
: provides a singlenull
argument to the annotated@ParameterizedTest
method.@NullSource
cannot be used for a parameter that has a primitive type.
@EmptySource
: provides a single empty argument to the annotated@ParameterizedTest
method for parameters of the following types:java.lang.String
,java.util.List
,java.util.Set
,java.util.Map
, primitive arrays (e.g.,int[]
,char[][]
, etc.), object arrays (e.g.,String[]
,Integer[][]
, etc.).- Subtypes of the supported types are not supported.
@NullAndEmptySource
: a composed annotation that combines the functionality of@NullSource
and@EmptySource
.
If you need to supply multiple varying types of blank strings to a parameterized test, you can achieve that using @ValueSource — for example, @ValueSource(strings = {" ", " ", "\t", "\n"})
.
You can also combine @NullSource
, @EmptySource
, and @ValueSource
to test a wider range of null
, empty, and blankinput. The following example demonstrates how to achieve this for strings.
@ParameterizedTest @NullSource @EmptySource @ValueSource(strings = { " ", " ", "\t", "\n" }) void nullEmptyAndBlankStrings(String text) { assertTrue(text == null || text.trim().isEmpty()); }
10.3 EnumSource
@EnumSource
provides a convenient way to use Enum
constants. The annotation provides an optional names
parameter that lets you specify which constants shall be used. If omitted, all constants will be used like in the following example.
@ParameterizedTest @EnumSource(TimeUnit.class) void testWithEnumSource(TimeUnit timeUnit) { assertNotNull(timeUnit); } @ParameterizedTest @EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" }) void testWithEnumSourceInclude(TimeUnit timeUnit) { assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit)); } @ParameterizedTest @EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" }) void testWithEnumSourceExclude(TimeUnit timeUnit) { assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit)); assertTrue(timeUnit.name().length() > 5); } @ParameterizedTest @EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$") void testWithEnumSourceRegex(TimeUnit timeUnit) { String name = timeUnit.name(); assertTrue(name.startsWith("M") || name.startsWith("N")); assertTrue(name.endsWith("SECONDS")); }
10.4 CsvSource
@CsvSource
allows you to express argument lists as comma-separated values (i.e., String
literals).
@ParameterizedTest @CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 0xF1" }) void testWithCsvSource(String fruit, int rank) { assertNotNull(fruit); assertNotEquals(0, rank); }
10.5 CsvFileSource
@CsvFileSource
lets you use CSV files from the classpath. Each line from a CSV file results in one invocation of the parameterized test.
@ParameterizedTest @CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1) void testWithCsvFileSource(String country, int reference) { assertNotNull(country); assertNotEquals(0, reference); }
11. Customizing Display Names
By default, the display name of a parameterized test invocation contains the invocation index and the String
representation of all arguments for that specific invocation. However, you can customize invocation display names via the name
attribute of the @ParameterizedTest
annotation like in the following example.
@DisplayName("Display name of container") @ParameterizedTest(name = "{index} ==> fruit=''{0}'', rank={1}") @CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" }) void testWithCustomDisplayNames(String fruit, int rank) { }
When executing the above method using the ConsoleLauncher
you will see output similar to the following.
Display name of container ✔ ├─ 1 ==> fruit='apple', rank=1 ✔ ├─ 2 ==> fruit='banana', rank=2 ✔ └─ 3 ==> fruit='lemon, lime', rank=3 ✔
12. Dynamic Tests
Dynamic on-the-fly tests can be made using @TestFactory and lambdas. These tests must return instances of Streams, Collections, Iterables, or Iterators of DynamicNode instances. These cases are executed lazily and so is generated at run-time.
@TestFactory Collection<DynamicTest> dynamicTestsFromCollection() { return Arrays.asList( dynamicTest("Test True", () -> assertTrue(true)), dynamicTest("Test Equals", () -> assertEquals(5, 2 + 3)) ); }
13. Lazy Initialization
As mentioned in the previous section we can now use lazy initialization thanks to allowance of Lambdas. These aren’t only used in @TestFactory test cases though they can be used to generate dynamic error messages. Meaning that if you have a method that takes a while to create the error message then it will only execute once the assertion fails instead of every time.
14. Multiple assertions at once
We now have the power to group assertions together using the assertAll method. It will process all of the assertions in a group even if one fails. Then it will let us know which assertions failed at the end so we do not have to fix and rerun one at a time.
assertAll( ()-> assertEquals(1, 2), () -> assertTrue(3 < 4), );
15. Assumptions
These were already present in JUnit 4 but I thought they were worth a mention because in JUnit 5 they can also use Java 8 lambdas. According to the JUnit 5 user guide “Assumptions provide a basic form of dynamic behavior but are intentionally rather limited in their expressiveness”. They are useful because a failed Assumption does not fail a test, it aborts it. This is useful if you only want to perform tests under certain conditions like on certain platforms or only if certain variables are present in the current runtime environment.
@Test void testOnlyOnCertainMachines() { assumeTrue("dev".equals(System.getenv("ENV")), () -> "Aborting test as not needed on this computer"); // rest of the test to run }
Migration from Junit 4
The following are topics that you should be aware of when migrating existing JUnit 4 tests to JUnit Jupiter.
- Annotations reside in the
org.junit.jupiter.api
package. - Assertions reside in
org.junit.jupiter.api.Assertions
. - Assumptions reside in
org.junit.jupiter.api.Assumptions
.- Note that JUnit Jupiter 5.4 and later versions support methods from JUnit 4’s
org.junit.Assume
class for assumptions. Specifically, JUnit Jupiter supports JUnit 4’sAssumptionViolatedException
to signal that a test should be aborted instead of marked as a failure.
- Note that JUnit Jupiter 5.4 and later versions support methods from JUnit 4’s
@Before
and@After
no longer exist; use@BeforeEach
and@AfterEach
instead.@BeforeClass
and@AfterClass
no longer exist; use@BeforeAll
and@AfterAll
instead.@Ignore
no longer exists: use@Disabled
or one of the other built-in execution conditions instead- See also JUnit 4 @Ignore Support.
@Category
no longer exists; use@Tag
instead.@RunWith
no longer exists; superseded by@ExtendWith
.@Rule
and@ClassRule
no longer exist; superseded by@ExtendWith
and@RegisterExtension
References: https://junit.org/junit5/docs/current/user-guide