Igor's Techno Club

5 Java JUnit 5 Features I Wish I Knew Earlier

junit5-underused-features

Since the release of JUnit 5 nearly ten years ago, numerous new features have been added, many of which are not well known to the broader community (myself included). Today, I will show you five features that I personally find extremely useful.

Temporary Directory Support

JUnit Version: 5.4

The @TempDir annotation provides a clean and efficient way to create and manage temporary directories for file-based tests. It automatically handles cleanup, reducing boilerplate code and potential resource leaks.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertTrue;

class TempDirTest {
    @Test
    void testUsingTempDirectory(@TempDir Path tempDir) throws IOException {
        Path testFile = tempDir.resolve("test.txt");
        Files.write(testFile, "Hello, World!".getBytes());
        assertTrue(Files.exists(testFile));
    }
}

Benefits:

If you are planning a migration from Java 8 to a newer version, take a look at this series of articles I have covering that topic

Parallel Test Execution

JUnit Version: 5.3

Parallel test execution can significantly speed up your test suite, especially for larger projects. By running tests concurrently, you can make better use of multi-core processors and reduce overall test execution time. It's good practice to write unit tests such that they can be executed in parallel: no shared states, no shared folders, etc. Even if you don't need to run them in parallel now, migrating to parallel execution in the future will be much smoother.

// In junit-platform.properties file
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent

// In test class
@Execution(ExecutionMode.CONCURRENT)
class ParallelExecutionTest {
    @Test
    void test1() { /* ... */ }
    @Test
    void test2() { /* ... */ }
}

Benefits:

Tag Expressions

JUnit Version: 5.0

Tag expressions allow for more complex filtering of tests, giving you fine-grained control over which tests to run. This is particularly useful in CI/CD pipelines where you might want to run different subsets of tests based on the build stage.

@Tag("integration")
@Tag("slow")
class TaggedTests {
    @Test
    void taggedTest() { /* ... */ }
}

// Run with: mvn test -Dgroups="integration & !slow"

Benefits:

Conditional Test Execution

JUnit Version: 5.1

Conditional test execution allows you to run or skip tests based on various conditions, such as system properties or environment variables. This feature is invaluable when dealing with platform-specific tests or environment-dependent scenarios. In my case, we use it to verify if a Kafka Broker exists by providing the KAFKA_HOST, KAFKA_PORT variable, and only then we run the tests.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;

public class SystemPropertyTests {

    @Test
    @EnabledIfSystemProperty(named = "os.name", matches = "Linux")
    void onlyOnLinux() {
        System.out.println("Running on Linux");
    }

    @Test
    @DisabledIfSystemProperty(named = "java.version", matches = "1.8.*")
    void notOnJava8() {
        System.out.println("Not running on Java 8");
    }
}
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;

public class EnvironmentVariableTests {

    @Test
    @EnabledIfEnvironmentVariable(named = "ENV", matches = "DEV")
    void onlyOnDevEnvironment() {
        System.out.println("Running in DEV environment");
    }

    @Test
    @DisabledIfEnvironmentVariable(named = "ENV", matches = "PROD")
    void notOnProdEnvironment() {
        System.out.println("Not running in PROD environment");
    }
}

Benefits:

Test Class Ordering

JUnit Version: 5.8

Test class ordering allows you to specify the execution order of nested test classes, which can be crucial when you have interdependent test scenarios or want to ensure a specific flow in your test suite.

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.junit.jupiter.api.ClassOrderer;

@TestClassOrder(ClassOrderer.OrderAnnotation.class)
public class OrderedTestClasses {

    @Nested
    @Order(1)
    class FirstTest {
        @Test
        void test() { /* ... */ }
    }

    @Nested
    @Order(2)
    class SecondTest {
        @Test
        void test() { /* ... */ }
    }
}

Benefits:

By using these often-overlooked features, we can write more effective, efficient, and maintainable tests. Each of these features addresses common pain points in testing, from performance issues to test organization and conditional execution. I encourage you to explore these features in your next JUnit 5 project and experience the benefits firsthand.

What's next

Checkout my latest post about Java

#java