JUnit 4 vs JUnit 5

JUnit 5 Improvements

  • JUnit 5 leverages features from Java 8 or later, such as lambda functions, making tests more powerful and easier to maintain.
  • JUnit 5 has added some very useful new features for describing, organizing, and executing tests. For instance, tests get better display names and can be organized hierarchically.
  • JUnit 5 is organized into multiple libraries, so only the features you need are imported into your project. With build systems such as Maven and Gradle, including the right libraries is easy.
  • JUnit 5 can use more than one extension at a time, which JUnit 4 could not (only one runner could be used at a time). This means you can easily combine the Spring extension with other extensions (such as your own custom extension).

Differences

Imports

  • org.junit.Test => org.junit.jupiter.api.Test
  • Assert => Assertions
    • org.junit.Assert => org.junit.jupiter.api.Assertions

Annotations

  • @Before => @BeforeEach

    • org.junit.Before => org.junit.jupiter.api.BeforeEach
  • @After => @AfterEach

  • @BeforeClass => @BeforeAll

  • @AfterClass => @AfterAll

  • @Ignore => @Disabled

    • org.junit.Ignore => org.junit.jupiter.api.Disabled
  • @Category => @Tag

  • @RunWith, @Rule, @ClassRule => @ExtendWith and @RegisterExtension

    • org.junit.runner.RunWith => org.junit.jupiter.api.extension.ExtendWith
    • @RunWith(SpringRunner.class) => @ExtendWith(SpringExtension.class)
    • org.springframework.test.context.junit4.SpringRunner => org.springframework.test.context.junit.jupiter.SpringExtension

Assertion Methods

JUnit 5 assertions are now in org.junit.jupiter.api.Assertions. Most of the common assertions, such as assertEquals() and assertNotNull(), look the same as before, but there are a few differences:

  • The error message is now the last argument, for example: assertEquals("my message", 1, 2) is now assertEquals(1, 2, "my message").
  • Most assertions now accept a lambda that constructs the error message, which is called only when the assertion fails.
  • assertTimeout() and assertTimeoutPreemptively() have replaced the @Timeout annotation (there is an @Timeout annotation in JUnit 5, but it works differently than in JUnit 4).
  • There are several new assertions, described below.

Note that you can continue to use assertions from JUnit 4 in a JUnit 5 test if you prefer.

Assumptions

Executes the supplied Executable, but only if the supplied assumption is valid.

JUnit 4

assumeThat("alwaysPasses", 1, is(1)); // passes
foo(); // will execute
assumeThat("alwaysFails", 0, is(1)); // assumption failure! test halts
int x = 1 / 0; // will never execute

JUnit 5

@Test
void testNothingInParticular() throws Exception {
Assumptions.assumingThat("DEV".equals(System.getenv("ENV")), () -> {
assertEquals(...);
});
}

Extending JUnit

JUnit 4

@RunWith(SpringRunner.class) // SpringRunner is an alias for the SpringJUnit4ClassRunner.
//@RunWith(SpringJUnit4ClassRunner.class)
public class MyControllerTest {
// ...
}

JUnit 5

@ExtendWith(SpringExtension.class)
class MyControllerTest {
// ...
}

Expect Exceptions

JUnit 4

@Test(expected = Exception.class)
public void testThrowsException() throws Exception {
// ...
}

JUnit 5

@Test
void testThrowsException() throws Exception {
Assertions.assertThrows(Exception.class, () -> {
//...
});
}

Timeout

JUnit 4

@Test(timeout = 10)
public void testFailWithTimeout() throws InterruptedException {
Thread.sleep(100);
}

JUnit 5

@Test
void testFailWithTimeout() throws InterruptedException {
Assertions.assertTimeout(Duration.ofMillis(10), () -> Thread.sleep(100));
}

Converting a Test to JUnit 5

To convert an existing JUnit 4 test to JUnit 5, use the following steps, which should work for most tests:

  1. Update imports to remove JUnit 4 and add JUnit 5. For instance, update the package name for the @Test annotation, and update both the package and class name for assertions (from Asserts to Assertions). Don’t worry yet if there are compilation errors, because completing the following steps should resolve them.
  2. Globally replace old annotations and class names with new ones. For example, replace all @Before with @BeforeEach and all Asserts with Assertions.
  3. Update assertions; any assertions that provide a message need to have the message argument moved to the end (pay special attention when all three arguments are strings!). Also, update timeouts and expected exceptions (see above for examples).
  4. Update assumptions if you are using them.
  5. Replace any instances of @RunWith, @Rule, or @ClassRule with the appropriate @ExtendWith annotations. You may need to find updated documentation online for the extensions you’re using for examples.

New Features

Display Names

you can add the @DisplayName annotation to classes and methods. The name is used when generating reports, which makes it easier to describe the purpose of tests and track down failures, for example:

@DisplayName("Test MyClass")
class MyClassTest {
@Test
@DisplayName("Verify MyClass.myMethod returns true")
void testMyMethod() throws Exception {
// ...
}
}

Assertion Methods

JUnit 5 introduced some new assertions, such as the following:

assertIterableEquals() performs a deep verification of two iterables using equals().

void assertIterableEquals(Iterable<?> expected, Iterable> actual)

assertLinesMatch() verifies that two lists of strings match; it accepts regular expressions in the expected argument.

void assertLinesMatch(List<String> expectedLines, List<String> actualLines)

assertAll() groups multiple assertions together. Asserts that all supplied executables do not throw exceptions. The added benefit is that all assertions are performed, even if individual assertions fail.

void assertAll(Executable... executables)

assertThrows() and assertDoesNotThrow() have replaced the expected property in the @Test annotation.

<T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable)
void assertDoesNotThrow (Executable executable)

Nested tests

Test suites in JUnit 4 were useful, but nested tests in JUnit 5 are easier to set up and maintain, and they better describe the relationships between test groups.

Parameterized tests

Test parameterization existed in JUnit 4, with built-in libraries such as JUnit4Parameterized or third-party libraries such as JUnitParams. In JUnit 5, parameterized tests are completely built in and adopt some of the best features from JUnit4Parameterized and JUnitParams, for example:

@ParameterizedTest
@ValueSource(strings = {"foo", "bar"})
@NullAndEmptySource
void myParameterizedTest(String arg) {
underTest.performAction(arg);
}

Conditional test execution

JUnit 5 provides the ExecutionCondition extension API to enable or disable a test or container (test class) conditionally. This is like using @Disabled on a test but it can define custom conditions. There are multiple built-in conditions, such as these:

  • @EnabledOnOs and @DisabledOnOs: Enables or disables a test only on specified operating systems
  • @EnabledOnJre and @DisabledOnJre: Specifies the test should be enabled or disabled for particular versions of Java
  • @EnabledIfSystemProperty: Enables a test based on the value of a JVM system property
  • @EnabledIf: Uses scripted logic to enable a test if scripted conditions are met

Test templates

Test templates are not regular tests; they define a set of steps to perform, which can then be executed elsewhere using a specific invocation context. This means that you can define a test template once, and then build a list of invocation contexts at runtime to run that test with. For details and examples, see the documentation.

Dynamic tests

Dynamic tests are like test templates; the tests to run are generated at runtime. However, while test templates are defined with a specific set of steps and run multiple times, dynamic tests use the same invocation context but can execute different logic. One use for dynamic tests would be to stream a list of abstract objects and perform a separate set of assertions for each based on their concrete types. There are good examples in the documentation.

Spring Boot Test With JUnit

Spring Boot Test With JUnit 4

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Starting with Spring Boot 2.4, JUnit 5’s vintage engine has been removed from spring-boot-starter-test. If we still want to write tests using JUnit 4, we need to add the following Maven dependency -->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
@RunWith(SpringRunner.class)
@SpringBootTest
public class MyServiceTest {
@Autowired
private MyRepository myRepository;

@org.junit.Test
public void test(){
}
}

Spring Boot Test With JUnit 5

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class MyServiceTest {
@Autowired
private MyRepository myRepository;

@org.junit.jupiter.api.Test
public void test(){
}
}

Conclusion

Although you probably won’t need to convert your old JUnit 4 tests to JUnit 5 unless you want to use new JUnit 5 features, there are compelling reasons to switch to JUnit 5.

References

Migrating from JUnit 4 to JUnit 5: Important Differences and Benefits

JUnit 5

JUnit 4