JUnit 5 Basics
JUnit 5 Basics
JUnit 5 is the de-facto standard testing framework on the JVM. It is not a single library but an architecture composed of three modules: JUnit Platform (the launcher and engine API), JUnit Jupiter (the new programming model and annotations you write every day), and JUnit Vintage (backwards-compatible runner for JUnit 3/4 suites). In day-to-day work you interact almost exclusively with Jupiter.
Adding JUnit 5 to Your Project
Modern build tools include JUnit 5 by default when you generate a project, but it is worth knowing the explicit dependency. In Maven add the BOM and a single test-scope dependency:
With Gradle (Kotlin DSL):
The @Test Annotation
@Test is the fundamental marker that turns an ordinary method into a test case. In Jupiter it lives in the org.junit.jupiter.api package — a different package from the old JUnit 4 org.junit.Test, so if your IDE imports the wrong one, the test runner ignores the method silently.
Rules for a valid @Test method:
- The class must be non-abstract and have a single accessible constructor (or none, triggering the default no-arg constructor).
- The method must be non-private and return void. JUnit 5 does not require
public— package-private is the preferred style. - The method must take no parameters unless you use parameter resolvers (covered later in this tutorial).
A First Test
Below is a self-contained class that tests a small utility:
reverseOfEmptyStringIsEmpty() tells you immediately what breaks if the test fails. Names like test1() or testReverse() tell you nothing useful. The JUnit Platform uses the method name as the display name in reports, so readability matters in source and in CI output.
How Tests Run
Understanding the execution model prevents a whole category of subtle ordering bugs.
- Discovery. The JUnit Platform scans the classpath for classes that contain
@Test-annotated methods. By convention these classes live undersrc/test/javaand mirror the package structure of the production code they exercise. - Instantiation. JUnit 5 creates a new instance of the test class for every test method by default. This deliberate design decision prevents shared mutable state from leaking between tests — a key source of flaky tests in JUnit 4, which reused one instance per class.
- Method execution. The platform invokes the method. If the method completes without throwing an exception the test passes. If any exception escapes — including
AssertionErrorfrom a failed assertion — the test is marked failed. - Reporting. Results are collected and surfaced by your IDE test runner, Maven Surefire, or Gradle's HTML test report.
Test Execution Order
By default JUnit 5 does not guarantee the order in which test methods execute within a class. The order is deterministic across runs (based on a hash of the method name), but not alphabetical or declaration order. This is intentional: tests that pass only in a specific order are hiding a hidden dependency and are not true unit tests.
If you need a specific order — for integration or scenario tests — annotate the class with @TestMethodOrder(MethodOrderer.OrderAnnotation.class) and then use @Order(1), @Order(2), etc. on each method. Reserve this for genuine integration scenarios, not as a crutch for poorly isolated unit tests.
Display Names
The @DisplayName annotation lets you replace the method name with a human-readable string including spaces, special characters, or emoji — useful when the test method name cannot fully express the intent:
test42() you lose that navigation. A good method name plus an optional @DisplayName for the report is the right combination.
Disabling a Test
Use @Disabled (with a mandatory reason string) when a test must temporarily be skipped — never delete it and never comment it out:
A disabled test still appears in the report as skipped, keeping visibility that something is intentionally not running. Deleting the test silently removes that visibility.
Summary
JUnit 5 Jupiter is the annotation layer you interact with daily. The @Test annotation marks a non-private, void, no-parameter method as a test case. A fresh class instance is created per method, eliminating inter-test pollution. Tests pass if no exception escapes and fail on any thrown exception including assertion failures. Good method names and the optional @DisplayName keep test reports readable. In the next lesson we will go deep on JUnit 5's assertion API, including grouped assertions, exception assertions, and timeout constraints.