1. Display Name Generators

JUnit 5 has support for customizing test class and test method names. We can use JUnit 5 custom display name generators via the @DisplayNameGeneration annotation. To get started, JUnit 5 provides a ReplaceUnderscores class that replaces any underscores in names with spaces. However, the @DisplayName annotation takes precedence over any display name generator.

@DisplayNameGeneration(ReplaceUnderscores.class)
class This_is_a_test_class {
    @Test
    void divisible_by_zero() {
        assertEquals(42 % 2, 0);
    }

    @Test
    @DisplayName("this is a negative check test")
    void is_negative() {
        assertTrue(-1 < 0);
    }
}

When we run the test, we can see that the output is more readable.

2. Parallel Execution

Parallel test execution is an experimental feature and is available as an opt-in since version 5.3. To enable parallel execution, set the following configuration parameters in junit-platform.properties

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent

Synchronization: JUnit 5 provides another annotation-based declarative synchronization mechanism. The @ResourceLock annotation allows you to declare that a test class or method uses a specific shared resource that requires synchronized access to ensure reliable test execution. The shared resource is identified by a unique name which is a String.

class SharedResourceTest {
    List<String> resources = new ArrayList<>();

    @Test
    @ResourceLock(value = "resources")
    void first() throws Exception {
        System.out.println("SharedResourceTest first() start => " + Thread.currentThread().getName());

        resources.add("first");
        Thread.sleep(500);
        assertEquals(1, resources.size());

        System.out.println("SharedResourceTest first() end => " + Thread.currentThread().getName());
    }

    @Test
    @ResourceLock(value = "resources")
    void second() throws Exception {
        System.out.println("SharedResourceTest second() start => " + Thread.currentThread().getName());

        resources.add("second");
        Thread.sleep(500);
        assertEquals(1, resources.size());

        System.out.println("SharedResourceTest second() end => " + Thread.currentThread().getName());
    }
}

log output:

SharedResourceTest second() start => ForkJoinPool-1-worker-2
SharedResourceTest second() end => ForkJoinPool-1-worker-2
SharedResourceTest first() start => ForkJoinPool-1-worker-3
SharedResourceTest first() end => ForkJoinPool-1-worker-3

3. Parameterized Tests

class SquareRootTest {
    @ParameterizedTest(name = "simple sqrt test: sqrt({0}) = sqrt({1})")
    @CsvSource(textBlock = """
        4.0, 2.0
        9.0, 3.0
        16.0, 4.0
    """)
    void sqrt(double input, double expected) {
        assertEquals(Math.sqrt(input), expected, 0.01);
    }
}

4. Repeated Test

When this test is run once or twice, it will pass without any issue. With the @RepeatedTest annotation, we can run it multiple times to make it fail. If you have code that you are unsure if there are any concurrency issues, this may be a way of testing it.

class ExampleRepeatedTest {
    @RepeatedTest(100)
    void flakyTest() {
        assertEquals(0.0, Math.random(), 0.9);
    }
}

5. Dynamic Test

Using the @TestFactory annotation, we can dynamically test all even numbers.

class ExampleDynamicTest {
    @TestFactory
    Stream<DynamicTest> evenNumbersAreDivisibleByTwo() {
        return IntStream.iterate(0, n -> n + 2)
                .limit(1_000)
                .mapToObj(n -> dynamicTest(n + " is divisible by 2",
                                () -> assertEquals(0, n % 2)));
    }
}