Introduction
JUnit is one of the most popular unit-testing frameworks in the Java ecosystem. The JUnit 5 version contains a number of exciting innovations, with the goal to support new features in Java 8 and above, as well as enabling many different styles of testing.
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
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) and build tools (see Gradle, Maven).
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 provides a TestEngine
for running JUnit 3 and JUnit 4 based tests on the platform.
What's new
Below you have just a taste of some of the new features JUnit 5 has got to offer. Check out the user guide for a more complete list.
@DisplayName
A display name can now be set for a test. This will be shown instead of the method name and will make output both easier to read and format. You can even include spaces and emojis.
@DisplayName("Single test successful") @Test void successTest() { log.info("Success"); }
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)) ); }
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.
@Test void multipleAssertionsTest() { assertAll( () -> assertEquals(1, 2), () -> assertTrue(3 < 4), ); }
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 }
ParameterizedTests
These allow you to run a test case multiple times with different arguments. These arguments can be strings, literal values, methods, Enums, CSV files, etc. @ParameterisedTest ultimately lets you avoid using unnecessary testing loops or duplicating test code.
@ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) void testWithValueSource(int argument) { assertTrue(argument > 0 && argument < 4); }
Declarative Timeouts@Timeout
isn’t limited to being placed on test cases themselves. Along with the aforementioned test cases, @Timeout
can also be placed at the type level, where it will provide a default timeout for all test cases declared in the class, this can be overridden by adding a @Timeout
to a test case. @Timeout
can also be added to lifecycle methods, @BeforeAll
, @BeforeEach
, @AfterEach
, @AfterAll
.
@Test @Timeout(unit = TimeUnit.MILLISECONDS, value = 500L) // Default unit is seconds, but other options available public void testTestCaseTimeout() throws InterruptedException { Thread.sleep(600); }
Asserting exceptions
The @Test
annotation no longer takes an expected argument. The Jupiter API provides a much tidier way of writing this type of test.
@Test void exceptionThrownIfTryToSetAlertStatusToNull() { IllegalArgumentException actual = assertThrows(IllegalArgumentException.class, () -> testee.setAlertStatus(null)); assertEquals("Alert status cannot be set to null.", actual.getMessage()); }
Temporary Directory Extension
The built-in TempDirectory
extension is used to create and clean up a temporary directory for an individual test or all tests in a test class. It is registered by default. To use it, annotate a non-private field of type java.nio.file.Path
orjava.io.File
with @TempDir
or add a parameter of type java.nio.file.Path
or java.io.File
annotated with @TempDir
to a lifecycle method or test method.
@Test void writeItemsToFile(@TempDir Path tempDir) throws IOException { Path file = tempDir.resolve("test.txt"); new ListWriter(file).write("a", "b", "c"); assertEquals(singletonList("a,b,c"), Files.readAllLines(file)); }
Declarative Extension Registration
Developers can register one or more extensions declaratively by annotating a test interface, test class, test method, or custom composed annotation with @ExtendWith(…)
and supplying class references for the extensions to register.
//creating a custom extension public class RandomParametersExtension implements ParameterResolver { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface Random { } @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return parameterContext.isAnnotated(Random.class); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return getRandomValue(parameterContext.getParameter(), extensionContext); } private Object getRandomValue(Parameter parameter, ExtensionContext extensionContext) { Class<?> type = parameter.getType(); java.util.Random random = extensionContext.getRoot().getStore(Namespace.GLOBAL)// .getOrComputeIfAbsent(java.util.Random.class); if (int.class.equals(type)) { return random.nextInt(); } if (double.class.equals(type)) { return random.nextDouble(); } throw new ParameterResolutionException("No random generator implemented for " + type); } } //using the custom extension @ExtendWith(RandomParametersExtension.class) class RandomParametersExtensionTests { @Test void injectsInteger(@Random int i, @Random int j) { assertNotEquals(i, j); } @Test void injectsDouble(@Random double d) { assertEquals(0.0, d, 1.0); } }
BeforeTestExecutionCallback
and AfterTestExecutionCallback
define the APIs for Extensions
that wish to add behavior that will be executed immediately before and immediately after a test method is executed, respectively. As such, these callbacks are well suited for timing, tracing, and similar use cases. If you need to implement callbacks that are invoked around @BeforeEach
and @AfterEach
methods, implement BeforeEachCallback
and AfterEachCallback
instead.
//creating a custom extension public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final Logger logger = Logger.getLogger(TimingExtension.class.getName()); private static final String START_TIME = "start time"; @Override public void beforeTestExecution(ExtensionContext context) throws Exception { getStore(context).put(START_TIME, System.currentTimeMillis()); } @Override public void afterTestExecution(ExtensionContext context) throws Exception { Method testMethod = context.getRequiredTestMethod(); long startTime = getStore(context).remove(START_TIME, long.class); long duration = System.currentTimeMillis() - startTime; logger.info(() -> String.format("Method [%s] took %s ms.", testMethod.getName(), duration)); } private Store getStore(ExtensionContext context) { return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod())); } } //using the custom extension @ExtendWith(TimingExtension.class) class TimingExtensionTests { @Test void sleep20ms() throws Exception { Thread.sleep(20); } @Test void sleep50ms() throws Exception { Thread.sleep(50); } }
Parallel Test execution
By default, JUnit Jupiter tests are run sequentially in a single thread. Running tests in parallel — for example, to speed up execution — is available as an opt-in feature since version 5.3. To enable parallel execution, set thejunit.jupiter.execution.parallel.enabled
configuration parameter to true
— for example, in junit-platform.properties
(see Configuration Parameters for other options). Please note that enabling this property is only the first step required to execute tests in parallel. If enabled, test classes and methods will still be executed sequentially by default. Whether or not a node in the test tree is executed concurrently is controlled by its execution mode. The following two modes are available.
- SAME_THREAD: Force execution in the same thread used by the parent. For example, when used on a test method, the test method will be executed in the same thread as any @BeforeAll or @AfterAll methods of the containing test class.
- CONCURRENT: Execute concurrently unless a resource lock forces execution in the same thread.
# Configuration parameters to execute top-level classes in parallel but methods in same thread junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = same_thread junit.jupiter.execution.parallel.mode.classes.default = concurrent or # Configuration parameters to execute top-level classes in sequentially but their methods in parallel junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = concurrent junit.jupiter.execution.parallel.mode.classes.default = same_thread or # Configuration parameters to execute top-level classes in parallel and their methods in parallel junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = concurrent junit.jupiter.execution.parallel.mode.classes.default = concurrent
Migration from JUnit 4
Although the JUnit Jupiter programming model and extension model will not support JUnit 4 features such as Rules
and Runners
natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custom build test infrastructure to migrate to JUnit Jupiter.
Maven dependencies
<dependencies> <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-engine</artifactId> <version>${junit.jupiter.version}</version> <scope>test</scope> </dependency> <dependency> <!-- temporary until all junit4 tests have been migrated --> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <!-- temporary until all junit4 tests have been migrated --> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <version>${junit.vintage.version}</version> <scope>test</scope> </dependency> </dependencies>
Running JUnit 4 Tests on the JUnit Platform
Just make sure that the junit-vintage-engine
artifact is in your test runtime path. In that case JUnit 4 tests will automatically be picked up by the JUnit Platform launcher.
Migration Tips
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.
@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 insteadSee 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
See also Limited JUnit 4 Rule Support.