Lesson 21 of 71
JUnit 5
JUnit 5 is the standard unit-testing framework for Java. This lesson covers how to write tests with its Jupiter API and how to run them from a plain main method, so every example below is a complete program with checked output. After it, a reader can write and organize the tests found in a typical production code base.
| Part | What it is |
|---|---|
| JUnit Platform | The launcher that discovers and runs tests; build tools and IDEs call it |
| JUnit Jupiter | The programming model: @Test, assertions, lifecycle, extensions |
| JUnit Vintage | An engine that runs old JUnit 3 and 4 tests on the Platform |
junit-jupiter-params |
Parameterized tests (@ParameterizedTest and argument sources) |
The examples use JUnit 5.13 (Platform 1.13) from the console standalone jar.
Example 1: a first test and the launcher helper
A test class is a class with methods annotated @Test. A test method returns void, takes no parameters (unless an extension supplies them), and fails when it throws. Jupiter test classes and methods may be package-private, which is the usual style.
Build tools normally run tests. Here the public class calls the JUnit Platform launcher API directly: LauncherDiscoveryRequestBuilder selects the test classes, LauncherFactory.create() returns a Launcher, and a SummaryGeneratingListener counts the results.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
class Calculator {
int add(int a, int b) { return a + b; }
}
class CalculatorTest {
Calculator calc = new Calculator();
@Test
void addsTwoNumbers() {
assertEquals(5, calc.add(2, 3));
}
@Test
void wrongExpectation() {
assertEquals(6, calc.add(2, 3), "2 + 3");
}
}
public class FirstTest {
public static void main(String[] args) {
run(CalculatorTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ String.valueOf(f.getException().getMessage()).lines().findFirst().orElse(""))
.sorted()
.forEach(System.out::println);
}
}tests: found=2 succeeded=1 failed=1 aborted=0 skipped=0
FAILED wrongExpectation(): 2 + 3 ==> expected: <6> but was: <5>The helper prints the counts and one line per failure, sorted by name, so the output does not depend on execution order. assertEquals(expected, actual, message) puts the expected value first; the optional last argument is prefixed to the failure message. Every later example in this lesson and in the next two lessons repeats the same run method unchanged at the end of the file.
Example 2: assertions
Assertions has static methods for each kind of check. Three of them need explanation:
assertThrows(type, executable)passes when the code throws that type (or a subtype) and returns the exception, so the test can check its message.assertAll(executables...)runs every check and reports all failures together, instead of stopping at the first one.assertTimeout(duration, executable)runs the code in the test thread, waits for it to finish, and fails if it took longer than the duration.assertTimeoutPreemptivelyruns the code in another thread and abandons it when time runs out.
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
record Money(long cents, String currency) {
Money {
if (cents < 0) throw new IllegalArgumentException("negative amount: " + cents);
}
}
class AssertionTests {
@Test
void throwsOnNegative() {
var e = assertThrows(IllegalArgumentException.class, () -> new Money(-1, "USD"));
assertEquals("negative amount: -1", e.getMessage());
}
@Test
void groupedChecksAllRun() {
Money m = new Money(500, "EUR");
assertAll("money",
() -> assertEquals(500, m.cents()),
() -> assertEquals("USD", m.currency()),
() -> assertTrue(m.cents() > 1000, "large"));
}
@Test
void finishesInTime() {
List<Integer> sorted = assertTimeout(Duration.ofSeconds(5),
() -> List.of(3, 1, 2).stream().sorted().toList());
assertIterableEquals(List.of(1, 2, 3), sorted);
}
@Test
void sameInstance() {
String a = "id";
assertNotSame(a, new String(a));
assertNull(null);
}
}
public class AssertionsDemo {
public static void main(String[] args) {
run(AssertionTests.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ String.valueOf(f.getException().getMessage()).lines().findFirst().orElse(""))
.sorted()
.forEach(System.out::println);
}
}tests: found=4 succeeded=3 failed=1 aborted=0 skipped=0
FAILED groupedChecksAllRun(): money (2 failures)The groupedChecksAllRun failure is a MultipleFailuresError. Its first message line names the heading and the number of failures; the full message lists both failed checks (USD and large), which a plain sequence of assertEquals calls would not show, since the first failure ends the method.
Example 3: lifecycle methods
Jupiter creates a new instance of the test class for every test method by default (Lifecycle.PER_METHOD). That keeps tests independent: a field changed by one test is not seen by the next. Lifecycle methods run around the tests:
| Annotation | Runs | Must be static by default |
|---|---|---|
@BeforeAll |
once, before all tests in the class | yes |
@BeforeEach |
before every test | no |
@AfterEach |
after every test, even a failed one | no |
@AfterAll |
once, after all tests | yes |
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.*;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
@TestMethodOrder(MethodOrderer.MethodName.class)
class LifecycleTest {
static final List<String> log = new ArrayList<>();
List<String> items = new ArrayList<>();
@BeforeAll static void openPool() { log.add("beforeAll"); }
@BeforeEach void setUp() { items.add("seed"); log.add("beforeEach"); }
@AfterEach void tearDown() { log.add("afterEach"); }
@AfterAll static void closePool() { log.add("afterAll"); }
@Test void a_addsOne() {
items.add("x");
assertEquals(2, items.size());
log.add("test a");
}
@Test void b_seesFreshList() {
assertEquals(1, items.size()); // a's "x" is not here: new instance
log.add("test b");
}
}
public class LifecycleDemo {
public static void main(String[] args) {
run(LifecycleTest.class);
LifecycleTest.log.forEach(System.out::println);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ String.valueOf(f.getException().getMessage()).lines().findFirst().orElse(""))
.sorted()
.forEach(System.out::println);
}
}tests: found=2 succeeded=2 failed=0 aborted=0 skipped=0
beforeAll
beforeEach
test a
afterEach
beforeEach
test b
afterEach
afterAll@TestMethodOrder(MethodOrderer.MethodName.class) fixes the order for this demonstration. Without it, the order is deterministic but intentionally not obvious, and tests must not depend on it. Annotating the class with @TestInstance(TestInstance.Lifecycle.PER_CLASS) makes Jupiter use one instance for all tests; then @BeforeAll and @AfterAll may be instance methods, and state can leak between tests.
Example 4: @Nested and @DisplayName
@Nested marks a non-static inner class as a group of tests. Each nested test runs the outer @BeforeEach methods first, then the nested ones, so a nested class describes one state of the object under test. @DisplayName replaces the method or class name in reports.
import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.*;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
@DisplayName("A stack")
class StackTest {
Deque<String> stack;
@BeforeEach void create() { stack = new ArrayDeque<>(); }
@Test @DisplayName("is empty when created")
void emptyOnCreate() { assertTrue(stack.isEmpty()); }
@Nested @DisplayName("after one push")
class AfterPush {
@BeforeEach void push() { stack.push("a"); }
@Test @DisplayName("pop returns the element")
void pop() { assertEquals("a", stack.pop()); }
@Test @DisplayName("is still empty (wrong on purpose)")
void wrong() { assertTrue(stack.isEmpty(), "expected empty"); }
@Nested @DisplayName("and one pop")
class AndPop {
@BeforeEach void pop() { stack.pop(); }
@Test @DisplayName("pop throws")
void popThrows() { assertThrows(NoSuchElementException.class, stack::pop); }
}
}
}
public class NestedDemo {
public static void main(String[] args) {
run(StackTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ String.valueOf(f.getException().getMessage()).lines().findFirst().orElse(""))
.sorted()
.forEach(System.out::println);
}
}tests: found=4 succeeded=3 failed=1 aborted=0 skipped=0
FAILED is still empty (wrong on purpose): expected empty ==> expected: <true> but was: <false>Selecting the outer class also runs its nested classes, which is why four tests are found. The failure line shows the display name, not the method name.
Example 5: parameterized tests
@ParameterizedTest runs one method once per set of arguments. Each run is a separate test in the counts. The argument source decides where the values come from:
| Source | Supplies |
|---|---|
@ValueSource |
one literal per run (ints, strings, and other simple types) |
@CsvSource |
rows of comma-separated values, converted to the parameter types |
@MethodSource |
a static factory method returning a Stream, List, or array of Arguments |
import static org.junit.jupiter.api.Assertions.*;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
class Fees {
static long feeCents(long amountCents) {
return amountCents < 10_000 ? 50 : amountCents / 100; // 1%, minimum 50
}
static boolean isValidIsin(String s) { return s != null && s.matches("[A-Z]{2}[A-Z0-9]{9}[0-9]"); }
}
class FeesTest {
@ParameterizedTest
@ValueSource(strings = {"US0378331005", "GB0002634946", "us0378331005"})
void validIsins(String isin) {
assertTrue(Fees.isValidIsin(isin));
}
@ParameterizedTest(name = "{0} cents -> fee {1}")
@CsvSource({
"500, 50",
"10000, 100",
"250000, 2500"
})
void fee(long amount, long expected) {
assertEquals(expected, Fees.feeCents(amount));
}
static Stream<Arguments> boundaries() {
return Stream.of(
Arguments.of(9_999L, 50L),
Arguments.of(10_001L, 100L));
}
@ParameterizedTest
@MethodSource("boundaries")
void boundary(long amount, long expected) {
assertEquals(expected, Fees.feeCents(amount));
}
}
public class ParamsDemo {
public static void main(String[] args) {
run(FeesTest.class);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ String.valueOf(f.getException().getMessage()).lines().findFirst().orElse(""))
.sorted()
.forEach(System.out::println);
}
}tests: found=8 succeeded=7 failed=1 aborted=0 skipped=0
FAILED [3] us0378331005: expected: <true> but was: <false>Eight invocations run. The lower-case ISIN fails, and the default display name [3] us0378331005 shows the invocation index and the argument, which makes the failing row easy to find. The name attribute on the CSV test sets a custom pattern where {0} and {1} are the arguments. @CsvSource trims whitespace around values, so the columns can be aligned.
Example 6: @TempDir, assumptions, and @Disabled
@TempDir on a field or parameter of type Path gives the test a fresh directory, which JUnit deletes after the test. Tests that write files then do not interfere with each other or leave files behind.
An assumption (Assumptions.assumeTrue, assumeFalse, assumingThat) stops a test without failing it when a precondition does not hold, for example a test that needs a specific operating system. A stopped test is counted as aborted. @Disabled skips a test without running it; it is counted as skipped.
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
class TradeFileWriter {
static void write(Path file, List<String> trades) throws IOException {
Files.write(file, trades);
}
}
class FileTests {
static Path seen;
@Test
void writesOneLinePerTrade(@TempDir Path dir) throws IOException {
seen = dir;
Path file = dir.resolve("trades.csv");
TradeFileWriter.write(file, List.of("AAPL,100", "MSFT,50"));
assertEquals(2, Files.readAllLines(file).size());
}
@Test
void onlyOnMars() {
assumeTrue("mars".equals(System.getProperty("os.name")), "not on Mars");
fail("never reached");
}
@Test
@Disabled("waiting for exchange sandbox")
void againstSandbox() {
fail("never runs");
}
}
public class TempDirDemo {
public static void main(String[] args) {
run(FileTests.class);
System.out.println("temp dir deleted: " + !Files.exists(FileTests.seen));
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ String.valueOf(f.getException().getMessage()).lines().findFirst().orElse(""))
.sorted()
.forEach(System.out::println);
}
}tests: found=3 succeeded=1 failed=0 aborted=1 skipped=1
temp dir deleted: trueThe aborted and skipped tests do not appear as failures, so a build passes. That is the intended behavior, and also the risk: a test that is always aborted on the build machine tests nothing.
Example 7: tags and filters
@Tag("name") labels a test class or method. The launcher, or a build tool configuration, includes or excludes tags with a filter. A common split is fast unit tests on every build and slow or integration tests in a separate stage. This example adds a TagFilter to the discovery request; the rest of the helper is unchanged.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Arrays;
import org.junit.jupiter.api.*;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.TagFilter;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
class PricingTest {
@Test void unitOne() { assertEquals(4, 2 + 2); }
@Test void unitTwo() { assertEquals("ab", "a" + "b"); }
@Test @Tag("slow") void fullDayReplay() { assertEquals(1, 1); }
@Test @Tag("integration") void database() { assertEquals(1, 1); }
}
public class TagsDemo {
public static void main(String[] args) {
System.out.println("exclude slow, integration:");
run(TagFilter.excludeTags("slow", "integration"));
System.out.println("include integration:");
run(TagFilter.includeTags("integration"));
}
static void run(org.junit.platform.launcher.PostDiscoveryFilter filter) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClass(PricingTest.class))
.filters(filter)
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
}
}exclude slow, integration:
tests: found=2 succeeded=2 failed=0 aborted=0 skipped=0
include integration:
tests: found=1 succeeded=1 failed=0 aborted=0 skipped=0A filtered-out test is not discovered at all, so it does not appear in any count. With Maven Surefire or Gradle, the same filter is set in the build file (groups/excludedGroups, or useJUnitPlatform { excludeTags(...) }).
Example 8: the extension model
An extension is a class that implements one or more callback interfaces and is registered with @ExtendWith. Jupiter calls it at defined points: BeforeAllCallback, BeforeEachCallback, AfterEachCallback, TestExecutionExceptionHandler, and ParameterResolver (which supplies test-method parameters) are common ones. @TempDir and the Mockito integration in the next lesson are built on this model.
This extension injects a fixed Clock into any test parameter of that type and records each finished test with its outcome.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.*;
import java.util.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.*;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
class FixedClockExtension implements ParameterResolver, AfterEachCallback {
static final List<String> audit = Collections.synchronizedList(new ArrayList<>());
@Override
public boolean supportsParameter(ParameterContext p, ExtensionContext c) {
return p.getParameter().getType() == Clock.class;
}
@Override
public Object resolveParameter(ParameterContext p, ExtensionContext c) {
return Clock.fixed(Instant.parse("2026-01-02T09:30:00Z"), ZoneOffset.UTC);
}
@Override
public void afterEach(ExtensionContext c) {
String outcome = c.getExecutionException().isPresent() ? "failed" : "passed";
audit.add(c.getRequiredTestMethod().getName() + " " + outcome);
}
}
@ExtendWith(FixedClockExtension.class)
class MarketHoursTest {
static boolean isOpen(Clock clock) {
LocalTime t = LocalTime.now(clock);
return !t.isBefore(LocalTime.of(9, 30)) && t.isBefore(LocalTime.of(16, 0));
}
@Test void openAtTheBell(Clock clock) { assertEquals(true, isOpen(clock)); }
@Test void dateIsFixed(Clock clock) { assertEquals(LocalDate.of(2026, 1, 2), LocalDate.now(clock)); }
@Test void wrongOnPurpose(Clock clock) { assertEquals(false, isOpen(clock)); }
}
public class ExtensionDemo {
public static void main(String[] args) {
run(MarketHoursTest.class);
FixedClockExtension.audit.stream().sorted().forEach(System.out::println);
}
static void run(Class<?>... testClasses) {
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).toList())
.build();
var listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(request, listener);
TestExecutionSummary s = listener.getSummary();
System.out.printf("tests: found=%d succeeded=%d failed=%d aborted=%d skipped=%d%n",
s.getTestsFoundCount(), s.getTestsSucceededCount(), s.getTestsFailedCount(),
s.getTestsAbortedCount(), s.getTestsSkippedCount());
s.getFailures().stream()
.map(f -> "FAILED " + f.getTestIdentifier().getDisplayName() + ": "
+ String.valueOf(f.getException().getMessage()).lines().findFirst().orElse(""))
.sorted()
.forEach(System.out::println);
}
}tests: found=3 succeeded=2 failed=1 aborted=0 skipped=0
FAILED wrongOnPurpose(Clock): expected: <false> but was: <true>
dateIsFixed passed
openAtTheBell passed
wrongOnPurpose failedWithout the extension, Jupiter would fail each test with a ParameterResolutionException, because it has no way to create a Clock. Injecting the clock is the testing-time technique covered in Testcontainers and Testing Concurrent Code. Extensions can also be registered programmatically with @RegisterExtension on a field, which allows constructor arguments, or globally through ServiceLoader when auto-detection is enabled.
Interview points
- JUnit 5 is three parts: Platform (launcher and engine API), Jupiter (the new programming model), and Vintage (runs JUnit 4 tests).
- Jupiter creates a new test-class instance per test method by default;
@TestInstance(PER_CLASS)shares one instance. @BeforeAll/@AfterAllare static unless the lifecycle is per class.assertAllreports every failed check; plain assertions stop at the first.assertTimeoutruns in the test thread and waits;assertTimeoutPreemptivelyruns elsewhere and abandons the code, which breaks thread-bound state such asThreadLocaltransactions.- A failed assumption aborts a test;
@Disabledskips it; neither fails the build. - Each parameterized invocation is a separate test with its own display name.
- Extensions replace JUnit 4 runners and rules; a class can have many extensions but only one runner.
- Test-method order is deterministic but unspecified; tests must not depend on it.
Summary
@Testmethods arevoid, may be package-private, and fail by throwing.- The launcher API (
LauncherDiscoveryRequestBuilder,LauncherFactory,SummaryGeneratingListener) runs tests without a build tool. - Lifecycle:
@BeforeAll,@BeforeEach, test,@AfterEach,@AfterAll. @Nestedinner classes group tests by state and inherit outer setup.@ValueSource,@CsvSource, and@MethodSourcefeed@ParameterizedTest.@TempDirgives a fresh directory that is deleted after the test.@Tagplus a tag filter selects groups of tests.ParameterResolverand callback interfaces are the extension points.
