Add junit-mockito module: 30 runnable JUnit 6.1.3 / Mockito 5.24.0 interview answers

One test per question, each writing and asserting its own transcript under output/.
Covers lifecycle, extensions, parameterized and dynamic tests, mocks vs spies,
verification, strict stubs, static and constructor mocking, and flaky tests.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CifR2BE7PBvRy5hT8TS9oz
This commit is contained in:
2026-10-26 01:52:44 +00:00
co-authored by Claude Sonnet 5.5
parent d03962ea9c
commit 715036b640
57 changed files with 1672 additions and 0 deletions
+1
View File
@@ -11,6 +11,7 @@ One module per article. This repository currently holds:
| [`collections`](collections/) | Top 40 Java Collections Interview Questions (HashMap Internals to Fail-Fast Iterators) | published | | [`collections`](collections/) | Top 40 Java Collections Interview Questions (HashMap Internals to Fail-Fast Iterators) | published |
| [`lld-parking-lot`](lld-parking-lot/) | Low-Level Design Interview in Java: Parking Lot System, Step by Step | published | | [`lld-parking-lot`](lld-parking-lot/) | Low-Level Design Interview in Java: Parking Lot System, Step by Step | published |
| [`hibernate-jpa`](hibernate-jpa/) | Top 40 Hibernate and JPA Interview Questions (2026) with Runnable Answers | this commit | | [`hibernate-jpa`](hibernate-jpa/) | Top 40 Hibernate and JPA Interview Questions (2026) with Runnable Answers | this commit |
| [`junit-mockito`](junit-mockito/) | Top 30 JUnit 6 and Mockito Interview Questions (2026) | this commit |
## `collections` module ## `collections` module
+21
View File
@@ -0,0 +1,21 @@
# junit-mockito
Companion code for **Top 30 JUnit 6 and Mockito Interview Questions (2026)** on ankurm.com.
One test method per question (`src/test/java/lab/jm/A_...F_*`, methods named `qNN_...`). Each test writes its own transcript to `output/qNN-*.txt` and asserts the same facts, so an answer in the article that stops being true fails the build. Questions 2 and 27 also have a script, because they need two different jar versions (Q2) and a JVM started without the Mockito agent (Q27).
| Component | Version |
|---|---|
| JUnit (Platform, Jupiter) | 6.1.3 |
| Mockito (`mockito-core`, `mockito-junit-jupiter`) | 5.24.0 |
| Maven Surefire | 3.6.0 |
| JDK | 25.0.5 (Temurin) |
```bash
./scripts/run-all.sh # runs the 28 test methods, then the two scripts; rewrites output/
mvn test # tests only
```
Test classes under `src/test/java/lab/jm/fixtures/` are launched on purpose through the JUnit Launcher API by the answer tests (to observe lifecycle order, failures, skips). Their names end in `Fixtures`, so Surefire does not run them directly; if your IDE offers to run them, some contain a deliberately failing test.
Surefire starts the test JVM with `-javaagent:` pointing at `mockito-core`, which is the setup Mockito recommends on current JDKs. `scripts/no-agent.sh` removes it to reproduce the warning described in Q27.
@@ -0,0 +1,6 @@
TestEngines found on the classpath (ServiceLoader<TestEngine>):
junit-jupiter 6.1.3
Jupiter API version : 6.1.3
Platform launcher : 6.1.3
Platform engine API : 6.1.3
junit-vintage engine : not on the classpath
@@ -0,0 +1,2 @@
junit-jupiter-api 5.14.4 : class-file major version 52 (Java 8), MethodOrderer$Alphanumeric entries in jar: 1
junit-jupiter-api 6.1.3 : class-file major version 61 (Java 17), MethodOrderer$Alphanumeric entries in jar: 0
@@ -0,0 +1,3 @@
class-file major version of org.junit.jupiter.api.Test : 61 (Java 17)
Class.forName("org.junit.jupiter.api.MethodOrderer$Alphanumeric") -> ClassNotFoundException
(the 5.14.4 versus 6.1.3 jar comparison is scripts/compare-5-vs-6.sh -> q02-jar-comparison.txt)
@@ -0,0 +1,8 @@
@BeforeAll (static)
@BeforeEach on instance #1
test second() on instance #1
@AfterEach on instance #1
@BeforeEach on instance #2
test first() on instance #2
@AfterEach on instance #2
@AfterAll (static)
@@ -0,0 +1,3 @@
@BeforeAll (non-static) on instance #1
second(): instance #1, counter=1
first(): instance #1, counter=2
@@ -0,0 +1,8 @@
outer @BeforeEach
outerTest()
outer @AfterEach
outer @BeforeEach
inner @BeforeEach
innerTest()
inner @AfterEach
outer @AfterEach
@@ -0,0 +1,4 @@
declared order in the source : zebra(), apple(), mango(), banana()
default (no orderer configured) : [banana(), apple(), mango(), zebra()]
MethodOrderer.MethodName : [apple(), banana(), mango(), zebra()]
MethodOrderer.DisplayName : [apple(), banana(), mango(), zebra()]
@@ -0,0 +1,5 @@
assertAll reported 2 failures from 3 assertions:
order (2 failures)
org.opentest4j.AssertionFailedError: quantity ==> expected: <1> but was: <2>
org.opentest4j.AssertionFailedError: code ==> expected: <A> but was: <B>
assertThrows returns the exception: getMessage() = boom
@@ -0,0 +1,4 @@
assertTimeout runs the code on the test thread : true
assertTimeoutPreemptively runs the code on the test thread : false
assertTimeout failure message (trimmed at ' by '): execution exceeded timeout of 50 ms
assertTimeoutPreemptively failure message: execution timed out after 50 ms
@@ -0,0 +1,3 @@
assertionFails() -> FAILED
assumptionFails() -> ABORTED
passes() -> SUCCESSFUL
@@ -0,0 +1,8 @@
declared: beforeAll
declared: beforeEach
test class: @BeforeEach method
declared: beforeTestExecution
test class: the test body
declared: afterTestExecution
declared: afterEach
declared: afterAll
@@ -0,0 +1,13 @@
declared: beforeAll
programmatic(static field): beforeAll
declared: beforeEach
programmatic(static field): beforeEach
declared: beforeTestExecution
programmatic(static field): beforeTestExecution
test body
programmatic(static field): afterTestExecution
declared: afterTestExecution
programmatic(static field): afterEach
declared: afterEach
programmatic(static field): afterAll
declared: afterAll
@@ -0,0 +1,2 @@
custom parameter : Tenant[name=tenant-for-resolvesCustomAndBuiltIn]
built-in TestInfo: displayName=resolvesCustomAndBuiltIn(Tenant, TestInfo)
@@ -0,0 +1,12 @@
enumSource(Level) / [1] LOW -> SUCCESSFUL
enumSource(Level) / [2] HIGH -> SUCCESSFUL
valueSource(int) / [1] 1 -> SUCCESSFUL
valueSource(int) / [2] 2 -> SUCCESSFUL
valueSource(int) / [3] 3 -> SUCCESSFUL
nullAndEmpty(String) / [1] null -> SUCCESSFUL
nullAndEmpty(String) / [2] "" -> SUCCESSFUL
nullAndEmpty(String) / [3] " " -> SUCCESSFUL
csvSource(String, int) / [1] "apple", "1" -> SUCCESSFUL
csvSource(String, int) / [2] "banana", "2" -> SUCCESSFUL
methodSource(String, int) / [1] "x", 10 -> SUCCESSFUL
methodSource(String, int) / [2] "y", 20 -> SUCCESSFUL
@@ -0,0 +1,11 @@
display name: csv(String, String) / [1] a="x" b=null -> SUCCESSFUL
display name: csv(String, String) / [2] a=null b="y" -> SUCCESSFUL
display name: csv(String, String) / [3] a="" b="z" -> SUCCESSFUL
display name: csv(String, String) / [4] a=null b="w" -> SUCCESSFUL
display name: csv(String, String) / [5] a="a,b" b="q" -> SUCCESSFUL
--- values the test method received ---
a="x" b=null
a=null b="y"
a="" b="z"
a=null b="w"
a="a,b" b="q"
@@ -0,0 +1,4 @@
@BeforeEach ran (1 time so far)
squares() / square of 1 -> SUCCESSFUL
squares() / square of 2 -> SUCCESSFUL
squares() / square of 3 -> SUCCESSFUL
@@ -0,0 +1,7 @@
--- no filter ---
disabled() -> SKIPPED (waiting for the fix in ticket 1234)
fast() -> SUCCESSFUL
onlyWithProperty() -> SKIPPED (System property [lab.run.slow] does not exist)
slow() -> SUCCESSFUL
--- includeTags("fast") ---
fast() -> SUCCESSFUL
+2
View File
@@ -0,0 +1,2 @@
inside the test : dir exists = true, files = 1
after the test finished : dir exists = false
+2
View File
@@ -0,0 +1,2 @@
mock: next() -> 0, next() -> 0, real code ran 0 times
spy : next() -> 1, next() -> 2, real code ran 2 times
@@ -0,0 +1,3 @@
when(spy.next()).thenReturn(100): real method ran while stubbing = 1 time(s)
doReturn(100).when(spy).next() : real method ran while stubbing = 0 time(s)
both return 100 and 100 after stubbing
@@ -0,0 +1,14 @@
times / atLeastOnce / never / inOrder: all passed
--- verify(gw).lookup("zzz") ---
Wanted but not invoked:
paymentGateway.lookup("zzz");
--- verify(gw).audit("declined c") ---
Argument(s) are different! Wanted:
paymentGateway.audit("declined c");
-> at lab.jm.D_MockitoCoreTest.lambda$q20_verify$1(D_MockitoCoreTest.java:90)
Actual invocations have different arguments:
paymentGateway.charge("a", 500L);
-> at lab.jm.OrderService.place(OrderService.java:17)
--- verify(gw, never()).audit("declined b") ---
paymentGateway.audit("declined b");
Never wanted here:
@@ -0,0 +1,5 @@
when(gw.charge("a", anyLong())) -> InvalidUseOfMatchersException
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at lab.jm.D_MockitoCoreTest.lambda$q21_matchersAndCaptor$0(D_MockitoCoreTest.java:103)
ArgumentCaptor saw account=zoe cents=1234
+11
View File
@@ -0,0 +1,11 @@
MockitoExtension (default strictness):
unusedStub() -> FAILED
org.mockito.exceptions.misusing.UnnecessaryStubbingException
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at lab.jm.fixtures.MockitoFixtures$StrictByDefault.unusedStub(MockitoFixtures.java:25)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
@MockitoSettings(strictness = LENIENT):
unusedStub() -> SUCCESSFUL
@@ -0,0 +1,4 @@
consecutive stubbing: true, false
third call throws : gateway down
void method via doThrow: audit store full
thenAnswer / BDD willAnswer: lookup("7") = user-7
@@ -0,0 +1,5 @@
FinalPricer is final: true
mock(FinalPricer.class).price(3) = 999 (real code would give 30)
mock.getClass() == FinalPricer.class : true
default MockMaker : org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker
class-file major version of org.mockito.Mockito : 55 (Java 11)
+4
View File
@@ -0,0 +1,4 @@
before : REAL-ID
inside the scope : MOCK-ID
other thread sees : REAL-ID
after the scope : REAL-ID
@@ -0,0 +1,4 @@
before : RECEIPT[tea]
inside : FAKE [tea]
constructed during the scope: 1
after : RECEIPT[tea]
@@ -0,0 +1,5 @@
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
WARNING: A Java agent has been loaded dynamically (~/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.7/byte-buddy-agent-1.17.7.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
@@ -0,0 +1,6 @@
int -> 0
boolean -> false
String -> null
List<String> -> []
Optional<String> -> Optional.empty
long -> 0
@@ -0,0 +1,10 @@
--- order a_, b_ (alphabetical) ---
a_registersUser() -> SUCCESSFUL
b_assumesUserExists() -> SUCCESSFUL
--- order b_, a_ (reversed) ---
b_assumesUserExists() -> FAILED
a_registersUser() -> SUCCESSFUL
AssertionFailedError: registry should contain the user registered by another test ==> expected: <1> but was: <0>
--- time: the same code with an injected Clock ---
acceptsOrders() at 10:00 UTC : true
acceptsOrders() at 23:30 UTC : false
@@ -0,0 +1,13 @@
failsOnThirdRepetition(RepetitionInfo) / repetition 1 of 6 -> SUCCESSFUL
failsOnThirdRepetition(RepetitionInfo) / repetition 2 of 6 -> SUCCESSFUL
failsOnThirdRepetition(RepetitionInfo) / repetition 3 of 6 -> FAILED
failsOnThirdRepetition(RepetitionInfo) / repetition 4 of 6 -> SKIPPED (Failure threshold [1] exceeded)
failsOnThirdRepetition(RepetitionInfo) / repetition 5 of 6 -> SKIPPED (Failure threshold [1] exceeded)
failsOnThirdRepetition(RepetitionInfo) / repetition 6 of 6 -> SKIPPED (Failure threshold [1] exceeded)
withoutThreshold(RepetitionInfo) / repetition 1 of 6 -> SUCCESSFUL
withoutThreshold(RepetitionInfo) / repetition 2 of 6 -> SUCCESSFUL
withoutThreshold(RepetitionInfo) / repetition 3 of 6 -> FAILED
withoutThreshold(RepetitionInfo) / repetition 4 of 6 -> SUCCESSFUL
withoutThreshold(RepetitionInfo) / repetition 5 of 6 -> SUCCESSFUL
withoutThreshold(RepetitionInfo) / repetition 6 of 6 -> SUCCESSFUL
failed=2 skipped=3
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ankurm.interview</groupId>
<artifactId>junit-mockito</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>6.1.3</junit.version>
<mockito.version>5.24.0</mockito.version>
<!-- Mockito's recommended setup on JDK 21+: load its agent at JVM start instead of self-attaching.
scripts/no-agent.sh overrides this property to reproduce the warning (Q27). -->
<mockito.agentArg>-javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar</mockito.agentArg>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<argLine>${mockito.agentArg} -Duser.timezone=UTC -Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Q2: compares the real junit-jupiter-api 5.14.4 and 6.1.3 jars. Writes output/q02-jar-comparison.txt.
# Needs JDK (javap, unzip) and Maven. Downloads both jars into the local Maven repository.
set -euo pipefail
cd "$(dirname "$0")/.."
unset JAVA_TOOL_OPTIONS
REPO="${HOME}/.m2/repository"
OUT=output/q02-jar-comparison.txt
TMP="$(mktemp -d)"
: > "$OUT"
for v in 5.14.4 6.1.3; do
mvn -B -q dependency:get -Dartifact="org.junit.jupiter:junit-jupiter-api:$v" >/dev/null
jar="$REPO/org/junit/jupiter/junit-jupiter-api/$v/junit-jupiter-api-$v.jar"
mkdir -p "$TMP/$v"
unzip -q -o "$jar" 'org/junit/jupiter/api/Test.class' -d "$TMP/$v"
major=$(javap -v -cp "$TMP/$v" org.junit.jupiter.api.Test | grep 'major version' | awk '{print $3}')
alpha=$(unzip -l "$jar" | grep -c 'MethodOrderer\$Alphanumeric' || true)
echo "junit-jupiter-api $v : class-file major version $major (Java $((major-44))), MethodOrderer\$Alphanumeric entries in jar: $alpha" | tee -a "$OUT"
done
rm -rf "$TMP"
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Q27: runs one Mockito test WITHOUT the -javaagent flag and keeps only the JVM's own warning lines.
# Writes output/q27-no-agent-warning.txt. Compare with the normal run, which prints none of these.
set -uo pipefail
cd "$(dirname "$0")/.."
unset JAVA_TOOL_OPTIONS
OUT=output/q27-no-agent-warning.txt
mvn -B test -Dtest='D_MockitoCoreTest#q18_mockVersusSpy' \
-Dmockito.agentArg=-Dlab.no.agent=true 2>&1 \
| grep -E '^(WARNING|Mockito is currently self-attaching)|dynamic(ally)? load|byte-buddy|-javaagent|EnableDynamicAgentLoading' \
| grep -v -E 'Unsafe|HiddenClassDefiner|guice' \
| sed -E "s#/[^ ]*/\.m2/repository#~/.m2/repository#g" > "$OUT"
cat "$OUT"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Regenerates every file in output/ (one per interview question).
# Requires JDK 25 and Maven. The first run must be online so Maven can fetch plugins and dependencies.
set -euo pipefail
cd "$(dirname "$0")/.."
unset JAVA_TOOL_OPTIONS
rm -f output/q*.txt
mvn -B -q test
./scripts/compare-5-vs-6.sh
./scripts/no-agent.sh
echo "done: $(ls output | wc -l) files in output/"
@@ -0,0 +1,16 @@
package lab.jm;
/** A real object with real behaviour, so a spy has something to call through to. */
public class Counter {
private int value;
private int realCalls;
public int next() {
realCalls++;
return ++value;
}
public int realCalls() {
return realCalls;
}
}
@@ -0,0 +1,8 @@
package lab.jm;
/** Final on purpose: Mockito 5's default (inline) mock maker can still mock it. */
public final class FinalPricer {
public int price(int quantity) {
return quantity * 10;
}
}
@@ -0,0 +1,11 @@
package lab.jm;
/** Static factory, the thing mockStatic exists for. */
public final class Ids {
private Ids() {
}
public static String newOrderId() {
return "REAL-ID";
}
}
@@ -0,0 +1,30 @@
package lab.jm;
import java.time.Clock;
import java.time.LocalTime;
/** Small service used by the Mockito answers: one collaborator, one Clock. */
public class OrderService {
private final PaymentGateway gateway;
private final Clock clock;
public OrderService(PaymentGateway gateway, Clock clock) {
this.gateway = gateway;
this.clock = clock;
}
public String place(String account, long cents) {
if (gateway.charge(account, cents)) {
gateway.audit("charged " + account + " " + cents);
return "OK";
}
gateway.audit("declined " + account);
return "DECLINED";
}
/** Depends on the wall clock: the classic source of time-of-day flakiness. */
public boolean acceptsOrders() {
LocalTime now = LocalTime.now(clock);
return !now.isBefore(LocalTime.of(9, 0)) && now.isBefore(LocalTime.of(17, 0));
}
}
@@ -0,0 +1,10 @@
package lab.jm;
/** Collaborator the service under test depends on. */
public interface PaymentGateway {
boolean charge(String account, long cents);
void audit(String message);
String lookup(String id);
}
@@ -0,0 +1,18 @@
package lab.jm;
/** Constructed inside a method under test, the thing mockConstruction exists for. */
public class Receipt {
private final String text;
public Receipt(String text) {
this.text = text;
}
public String render() {
return "RECEIPT[" + text + "]";
}
public static String print(String text) {
return new Receipt(text).render();
}
}
@@ -0,0 +1,171 @@
package lab.jm;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import lab.jm.fixtures.LifecycleFixtures;
import lab.jm.fixtures.Log;
import org.junit.jupiter.api.Test;
import org.opentest4j.MultipleFailuresError;
import org.junit.platform.engine.TestEngine;
/** Q1 to Q9: JUnit 6 fundamentals, lifecycle, ordering, assertions. */
class A_JUnitFundamentalsTest extends Lab {
@Test
void q01_platformJupiterVintage() {
q(1, "platform-jupiter-vintage");
List<String> engines = new ArrayList<>();
for (TestEngine e : ServiceLoader.load(TestEngine.class)) {
engines.add(e.getId() + " " + e.getVersion().orElse("?"));
}
p("TestEngines found on the classpath (ServiceLoader<TestEngine>):");
engines.forEach(e -> p(" %s", e));
p("Jupiter API version : %s", org.junit.jupiter.api.Test.class.getPackage().getImplementationVersion());
p("Platform launcher : %s", org.junit.platform.launcher.Launcher.class.getPackage().getImplementationVersion());
p("Platform engine API : %s", TestEngine.class.getPackage().getImplementationVersion());
p("junit-vintage engine : %s", engines.stream().anyMatch(e -> e.startsWith("junit-vintage")) ? "present" : "not on the classpath");
assertEquals(1, engines.size());
assertTrue(engines.get(0).startsWith("junit-jupiter 6."));
assertEquals(org.junit.jupiter.api.Test.class.getPackage().getImplementationVersion(),
org.junit.platform.launcher.Launcher.class.getPackage().getImplementationVersion());
}
@Test
void q02_removedAndBaseline() throws Exception {
q(2, "junit6-baseline-and-removals");
int major;
try (InputStream in = org.junit.jupiter.api.Test.class.getResourceAsStream("Test.class")) {
byte[] head = in.readNBytes(8);
major = ((head[6] & 0xff) << 8) | (head[7] & 0xff);
}
p("class-file major version of org.junit.jupiter.api.Test : %d (Java %d)", major, major - 44);
String removed = "org.junit.jupiter.api.MethodOrderer$Alphanumeric";
try {
Class.forName(removed);
p("%s : still present", removed);
} catch (ClassNotFoundException e) {
p("Class.forName(\"%s\") -> ClassNotFoundException", removed);
}
assertEquals(61, major);
assertThrows(ClassNotFoundException.class, () -> Class.forName(removed));
p("(the 5.14.4 versus 6.1.3 jar comparison is scripts/compare-5-vs-6.sh -> q02-jar-comparison.txt)");
}
@Test
void q03_lifecycleOrder() {
q(3, "lifecycle-order");
Log.clear();
LifecycleFixtures.PerMethod.instances = 0;
Run run = Run.of(LifecycleFixtures.PerMethod.class);
Log.lines().forEach(l -> p("%s", l));
assertEquals(2, LifecycleFixtures.PerMethod.instances, "one test instance per test method");
assertEquals("@BeforeAll (static)", Log.lines().get(0));
assertTrue(run.failures.isEmpty());
}
@Test
void q04_perClassLifecycle() {
q(4, "per-class-lifecycle");
Log.clear();
LifecycleFixtures.PerClass.instances = 0;
Run.of(LifecycleFixtures.PerClass.class);
Log.lines().forEach(l -> p("%s", l));
assertEquals(1, LifecycleFixtures.PerClass.instances);
assertTrue(Log.lines().get(2).endsWith("counter=2"));
}
@Test
void q05_nestedLifecycle() {
q(5, "nested-lifecycle");
Log.clear();
Run run = Run.of(LifecycleFixtures.Outer.class);
Log.lines().forEach(l -> p("%s", l));
List<String> l = Log.lines();
int inner = l.indexOf("inner @BeforeEach");
assertTrue(inner > 0 && "outer @BeforeEach".equals(l.get(inner - 1)), "outer runs right before inner");
assertTrue(run.failures.isEmpty());
}
@Test
void q06_methodOrder() {
q(6, "method-order");
Map<String, String> byName = Map.of("junit.jupiter.testmethod.order.default",
org.junit.jupiter.api.MethodOrderer.MethodName.class.getName());
Map<String, String> byDisplay = Map.of("junit.jupiter.testmethod.order.default",
org.junit.jupiter.api.MethodOrderer.DisplayName.class.getName());
Run dflt = Run.of(LifecycleFixtures.Ordering.class);
Run name = Run.of(LifecycleFixtures.Ordering.class, byName);
Run disp = Run.of(LifecycleFixtures.Ordering.class, byDisplay);
p("declared order in the source : zebra(), apple(), mango(), banana()");
p("default (no orderer configured) : %s", names(dflt));
p("MethodOrderer.MethodName : %s", names(name));
p("MethodOrderer.DisplayName : %s", names(disp));
assertEquals("[apple(), banana(), mango(), zebra()]", names(name));
assertEquals(4, dflt.events.size());
assertNotEquals("[zebra(), apple(), mango(), banana()]", names(name));
}
private static String names(Run r) {
return r.events.stream().map(e -> e.substring(0, e.indexOf(" ->"))).toList().toString();
}
@Test
void q07_groupedAssertions() {
q(7, "assert-all-and-assert-throws");
MultipleFailuresError e = assertThrows(MultipleFailuresError.class, () -> assertAll("order",
() -> assertEquals(1, 2, "quantity"),
() -> assertEquals("A", "B", "code"),
() -> assertTrue(true)));
p("assertAll reported %d failures from 3 assertions:", e.getFailures().size());
p("%s", e.getMessage());
IllegalStateException ise = assertThrows(IllegalStateException.class, () -> {
throw new IllegalStateException("boom");
});
p("assertThrows returns the exception: getMessage() = %s", ise.getMessage());
assertEquals(2, e.getFailures().size());
}
@Test
void q08_timeoutVsPreemptive() {
q(8, "timeout-vs-preemptive");
String test = Thread.currentThread().getName();
String[] seenBy = new String[2];
org.junit.jupiter.api.Assertions.assertTimeout(java.time.Duration.ofSeconds(1),
() -> seenBy[0] = Thread.currentThread().getName());
org.junit.jupiter.api.Assertions.assertTimeoutPreemptively(java.time.Duration.ofSeconds(1),
() -> seenBy[1] = Thread.currentThread().getName());
p("assertTimeout runs the code on the test thread : %s", seenBy[0].equals(test));
p("assertTimeoutPreemptively runs the code on the test thread : %s", seenBy[1].equals(test));
var slow1 = assertThrows(org.opentest4j.AssertionFailedError.class,
() -> org.junit.jupiter.api.Assertions.assertTimeout(java.time.Duration.ofMillis(50),
() -> Thread.sleep(200)));
String m1 = slow1.getMessage();
p("assertTimeout failure message (trimmed at ' by '): %s", m1.substring(0, m1.indexOf(" by ")));
var slow2 = assertThrows(org.opentest4j.AssertionFailedError.class,
() -> org.junit.jupiter.api.Assertions.assertTimeoutPreemptively(java.time.Duration.ofMillis(50),
() -> Thread.sleep(2000)));
p("assertTimeoutPreemptively failure message: %s", slow2.getMessage());
assertTrue(seenBy[0].equals(test));
assertFalse(seenBy[1].equals(test));
}
@Test
void q09_assertionVsAssumption() {
q(9, "assertion-vs-assumption");
Run run = Run.of(LifecycleFixtures.Outcomes.class);
run.events.stream().sorted().forEach(e -> p("%s", e));
assertTrue(run.events.contains("assertionFails() -> FAILED"));
assertTrue(run.events.contains("assumptionFails() -> ABORTED"));
}
}
@@ -0,0 +1,44 @@
package lab.jm;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import lab.jm.fixtures.ExtensionFixtures;
import lab.jm.fixtures.Log;
import org.junit.jupiter.api.Test;
/** Q10 to Q12: the extension model. */
class B_ExtensionsTest extends Lab {
@Test
void q10_callbackOrder() {
q(10, "extension-callback-order");
Log.clear();
Run.of(ExtensionFixtures.CallbackOrder.class);
Log.lines().forEach(l -> p("%s", l));
var l = Log.lines();
assertTrue(l.indexOf("declared: beforeEach") < l.indexOf("test class: @BeforeEach method"));
assertTrue(l.indexOf("test class: @BeforeEach method") < l.indexOf("declared: beforeTestExecution"));
assertTrue(l.indexOf("test class: the test body") < l.indexOf("declared: afterTestExecution"));
}
@Test
void q11_extendWithVsRegisterExtension() {
q(11, "extend-with-vs-register-extension");
Log.clear();
Run.of(ExtensionFixtures.Registration.class);
Log.lines().forEach(l -> p("%s", l));
var l = Log.lines();
assertEquals(2, l.stream().filter(x -> x.endsWith("beforeEach")).count());
}
@Test
void q12_parameterResolver() {
q(12, "parameter-resolver");
Log.clear();
Run run = Run.of(ExtensionFixtures.Injection.class);
Log.lines().forEach(l -> p("%s", l));
assertTrue(run.failures.isEmpty());
assertEquals("custom parameter : Tenant[name=tenant-for-resolvesCustomAndBuiltIn]", Log.lines().get(0));
}
}
@@ -0,0 +1,73 @@
package lab.jm;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.util.Map;
import lab.jm.fixtures.Log;
import lab.jm.fixtures.ParamFixtures;
import org.junit.jupiter.api.Test;
import org.junit.platform.launcher.TagFilter;
/** Q13 to Q17: parameterized, dynamic, conditional and temp-dir tests. */
class C_ParameterizedAndDynamicTest extends Lab {
@Test
void q13_parameterizedSources() {
q(13, "parameterized-sources");
Run run = Run.of(ParamFixtures.Sources.class);
run.events.forEach(e -> p("%s", e));
assertEquals(12, run.events.size());
assertTrue(run.events.stream().allMatch(e -> e.endsWith("SUCCESSFUL")));
}
@Test
void q14_csvConversion() {
q(14, "csv-null-and-empty");
Log.clear();
Run run = Run.of(ParamFixtures.CsvConversion.class);
run.events.forEach(e -> p("display name: %s", e));
p("--- values the test method received ---");
Log.lines().forEach(l -> p("%s", l));
assertTrue(Log.lines().contains("a=\"x\" b=null"));
assertTrue(Log.lines().contains("a=null b=\"y\""));
assertTrue(Log.lines().contains("a=\"\" b=\"z\""));
assertTrue(Log.lines().contains("a=null b=\"w\""));
}
@Test
void q15_dynamicTests() {
q(15, "dynamic-tests");
Log.clear();
Run run = Run.of(ParamFixtures.Dynamic.class);
Log.lines().forEach(l -> p("%s", l));
run.events.forEach(e -> p("%s", e));
assertEquals(1, Log.lines().size(), "@BeforeEach runs once per factory, not once per dynamic test");
}
@Test
void q16_disabledConditionalTags() {
q(16, "disabled-conditional-tags");
Run all = Run.of(ParamFixtures.Conditions.class);
p("--- no filter ---");
all.events.stream().sorted().forEach(e -> p("%s", e));
Run fast = Run.of(ParamFixtures.Conditions.class, Map.of(), TagFilter.includeTags("fast"));
p("--- includeTags(\"fast\") ---");
fast.events.forEach(e -> p("%s", e));
assertTrue(all.events.stream().anyMatch(e -> e.startsWith("disabled() -> SKIPPED")));
assertEquals(1, fast.events.size());
assertEquals(4, all.events.size());
}
@Test
void q17_tempDir() throws Exception {
q(17, "temp-dir");
Log.clear();
Run.of(ParamFixtures.TempDirs.class);
Log.lines().forEach(l -> p("%s", l));
p("after the test finished : dir exists = %s", Files.exists(ParamFixtures.TempDirs.seen));
assertFalse(Files.exists(ParamFixtures.TempDirs.seen));
}
}
@@ -0,0 +1,188 @@
package lab.jm;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import lab.jm.fixtures.MockitoFixtures;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.exceptions.misusing.InvalidUseOfMatchersException;
import org.mockito.exceptions.verification.opentest4j.ArgumentsAreDifferent;
import org.mockito.exceptions.verification.WantedButNotInvoked;
import org.mockito.exceptions.verification.NeverWantedButInvoked;
/** Q18 to Q23 and Q28: Mockito core behaviour. */
class D_MockitoCoreTest extends Lab {
private static String firstLines(Throwable t, int n) {
String[] parts = t.getMessage().strip().split("\n");
return String.join("\n", java.util.Arrays.copyOf(parts, Math.min(n, parts.length)));
}
@Test
void q18_mockVersusSpy() {
q(18, "mock-vs-spy");
Counter c1 = mock(Counter.class);
Counter c2 = spy(new Counter());
p("mock: next() -> %d, next() -> %d, real code ran %d times", c1.next(), c1.next(), c1.realCalls());
p("spy : next() -> %d, next() -> %d, real code ran %d times", c2.next(), c2.next(), c2.realCalls());
assertEquals(0, c1.next());
assertEquals(3, c2.next());
assertEquals(0, c1.realCalls());
assertEquals(3, c2.realCalls());
}
@Test
void q19_whenVersusDoReturnOnSpy() {
q(19, "when-vs-doreturn-on-spy");
Counter viaWhen = spy(new Counter());
when(viaWhen.next()).thenReturn(100);
p("when(spy.next()).thenReturn(100): real method ran while stubbing = %d time(s)", viaWhen.realCalls());
Counter viaDo = spy(new Counter());
doReturn(100).when(viaDo).next();
p("doReturn(100).when(spy).next() : real method ran while stubbing = %d time(s)", viaDo.realCalls());
p("both return %d and %d after stubbing", viaWhen.next(), viaDo.next());
assertEquals(1, viaWhen.realCalls());
assertEquals(0, viaDo.realCalls());
}
@Test
void q20_verify() {
q(20, "verify-times-never-inorder");
PaymentGateway gw = mock(PaymentGateway.class);
when(gw.charge("a", 500)).thenReturn(true);
OrderService svc = new OrderService(gw, Clock.systemUTC());
svc.place("a", 500);
svc.place("b", 700);
svc.place("a", 900);
verify(gw, times(2)).charge(eq("a"), anyLong());
verify(gw, atLeastOnce()).audit("declined b");
verify(gw, never()).lookup("x");
InOrder order = inOrder(gw);
order.verify(gw).charge("a", 500);
order.verify(gw).audit("charged a 500");
order.verify(gw).charge("b", 700);
p("times / atLeastOnce / never / inOrder: all passed");
Throwable e1 = assertThrows(WantedButNotInvoked.class, () -> verify(gw).lookup("zzz"));
p("--- verify(gw).lookup(\"zzz\") ---");
p("%s", firstLines(e1, 2));
Throwable e2 = assertThrows(ArgumentsAreDifferent.class, () -> verify(gw).audit("declined c"));
p("--- verify(gw).audit(\"declined c\") ---");
p("%s", firstLines(e2, 6));
Throwable e3 = assertThrows(NeverWantedButInvoked.class, () -> verify(gw, never()).audit("declined b"));
p("--- verify(gw, never()).audit(\"declined b\") ---");
p("%s", firstLines(e3, 2));
}
@Test
void q21_matchersAndCaptor() {
q(21, "matchers-and-captor");
PaymentGateway gw = mock(PaymentGateway.class);
InvalidUseOfMatchersException mixed = assertThrows(InvalidUseOfMatchersException.class,
() -> when(gw.charge("a", anyLong())).thenReturn(true));
p("when(gw.charge(\"a\", anyLong())) -> %s", mixed.getClass().getSimpleName());
p("%s", firstLines(mixed, 3));
OrderService svc = new OrderService(gw, Clock.systemUTC());
svc.place("zoe", 1234);
ArgumentCaptor<String> account = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<Long> cents = ArgumentCaptor.forClass(Long.class);
verify(gw).charge(account.capture(), cents.capture());
p("ArgumentCaptor saw account=%s cents=%d", account.getValue(), cents.getValue());
assertEquals("zoe", account.getValue());
assertEquals(1234L, cents.getValue());
}
@Test
void q22_strictStubs() {
q(22, "strict-stubs");
Run strict = Run.of(MockitoFixtures.StrictByDefault.class);
p("MockitoExtension (default strictness):");
strict.events.forEach(e -> p(" %s", e));
strict.failures.forEach(f -> {
p(" %s", f.getClass().getName());
for (String line : f.getMessage().split("\n")) {
p(" %s", line);
}
});
Run lenient = Run.of(MockitoFixtures.Lenient.class);
p("@MockitoSettings(strictness = LENIENT):");
lenient.events.forEach(e -> p(" %s", e));
assertEquals("unusedStub() -> FAILED", strict.events.get(0));
assertEquals("unusedStub() -> SUCCESSFUL", lenient.events.get(0));
}
@Test
void q23_stubbingStyles() {
q(23, "stubbing-styles");
PaymentGateway gw = mock(PaymentGateway.class);
when(gw.charge("a", 1)).thenReturn(true, false).thenThrow(new IllegalStateException("gateway down"));
p("consecutive stubbing: %s, %s", gw.charge("a", 1), gw.charge("a", 1));
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> gw.charge("a", 1));
p("third call throws : %s", ex.getMessage());
doThrow(new RuntimeException("audit store full")).when(gw).audit("x");
RuntimeException v = assertThrows(RuntimeException.class, () -> gw.audit("x"));
p("void method via doThrow: %s", v.getMessage());
given(gw.lookup("7")).willAnswer(inv -> "user-" + inv.getArgument(0));
p("thenAnswer / BDD willAnswer: lookup(\"7\") = %s", gw.lookup("7"));
assertEquals("user-7", gw.lookup("7"));
// Stubbing a void method with when(...) does not compile, which is why doThrow/doNothing exist.
}
@Test
void q28_unstubbedDefaults() {
q(28, "unstubbed-defaults");
Probe m = mock(Probe.class);
p("int -> %s", m.number());
p("boolean -> %s", m.flag());
p("String -> %s", m.text());
p("List<String> -> %s", m.list());
p("Optional<String> -> %s", m.opt());
p("long -> %s", m.big());
assertEquals(0, m.number());
assertEquals(List.of(), m.list());
assertEquals(Optional.empty(), m.opt());
assertTrue(m.text() == null);
}
interface Probe {
int number();
boolean flag();
String text();
List<String> list();
Optional<String> opt();
long big();
}
@Test
void clockIsFixedForTimeDependentCode() {
// used by the flaky-test answers; kept here so the Clock stays a collaborator, not a static call
Clock noon = Clock.fixed(Instant.parse("2026-10-26T12:00:00Z"), ZoneOffset.UTC);
assertTrue(new OrderService(mock(PaymentGateway.class), noon).acceptsOrders());
}
}
@@ -0,0 +1,74 @@
package lab.jm;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
/** Q24 to Q26: final classes, static mocking, constructor mocking. */
class E_MockitoAdvancedTest extends Lab {
@Test
void q24_finalClassesAndMockMaker() throws Exception {
q(24, "final-classes-mock-maker");
FinalPricer pricer = mock(FinalPricer.class);
when(pricer.price(3)).thenReturn(999);
p("FinalPricer is final: %s", java.lang.reflect.Modifier.isFinal(FinalPricer.class.getModifiers()));
p("mock(FinalPricer.class).price(3) = %d (real code would give 30)", pricer.price(3));
p("mock.getClass() == FinalPricer.class : %s", pricer.getClass() == FinalPricer.class);
var maker = org.mockito.internal.configuration.plugins.Plugins.getMockMaker();
p("default MockMaker : %s", maker.getClass().getName());
int major;
try (InputStream in = org.mockito.Mockito.class.getResourceAsStream("Mockito.class")) {
byte[] head = in.readNBytes(8);
major = ((head[6] & 0xff) << 8) | (head[7] & 0xff);
}
p("class-file major version of org.mockito.Mockito : %d (Java %d)", major, major - 44);
assertEquals(999, pricer.price(3));
assertTrue(pricer.getClass() == FinalPricer.class);
assertTrue(maker.getClass().getSimpleName().contains("Inline"));
assertEquals(55, major);
}
@Test
void q25_mockStatic() throws Exception {
q(25, "mock-static");
p("before : %s", Ids.newOrderId());
AtomicReference<String> otherThread = new AtomicReference<>();
try (MockedStatic<Ids> ids = mockStatic(Ids.class)) {
ids.when(Ids::newOrderId).thenReturn("MOCK-ID");
p("inside the scope : %s", Ids.newOrderId());
Thread t = new Thread(() -> otherThread.set(Ids.newOrderId()));
t.start();
t.join();
p("other thread sees : %s", otherThread.get());
ids.verify(Ids::newOrderId, org.mockito.Mockito.times(1));
}
p("after the scope : %s", Ids.newOrderId());
assertEquals("REAL-ID", otherThread.get());
assertEquals("REAL-ID", Ids.newOrderId());
}
@Test
void q26_mockConstruction() {
q(26, "mock-construction");
p("before : %s", Receipt.print("tea"));
try (MockedConstruction<Receipt> mc = mockConstruction(Receipt.class,
(mock, ctx) -> when(mock.render()).thenReturn("FAKE " + ctx.arguments()))) {
p("inside : %s", Receipt.print("tea"));
p("constructed during the scope: %d", mc.constructed().size());
assertEquals(1, mc.constructed().size());
}
p("after : %s", Receipt.print("tea"));
assertFalse(Receipt.print("tea").startsWith("FAKE"));
}
}
@@ -0,0 +1,56 @@
package lab.jm;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Map;
import lab.jm.fixtures.FlakyFixtures;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
/** Q29 and Q30: flaky tests. */
class F_FlakyTest extends Lab {
@Test
void q29_orderDependenceAndTime() {
q(29, "flaky-order-and-time");
FlakyFixtures.SharedState.REGISTRY.clear();
Run forward = Run.of(FlakyFixtures.SharedState.class,
Map.of("junit.jupiter.testmethod.order.default", MethodOrderer.MethodName.class.getName()));
FlakyFixtures.SharedState.REGISTRY.clear();
Run reverse = Run.of(FlakyFixtures.SharedState.class,
Map.of("junit.jupiter.testmethod.order.default", FlakyFixtures.ReverseOrder.class.getName()));
p("--- order a_, b_ (alphabetical) ---");
forward.events.forEach(e -> p("%s", e));
p("--- order b_, a_ (reversed) ---");
reverse.events.forEach(e -> p("%s", e));
reverse.failures.forEach(f -> p(" %s: %s", f.getClass().getSimpleName(), f.getMessage()));
PaymentGateway gw = mock(PaymentGateway.class);
Clock morning = Clock.fixed(Instant.parse("2026-10-26T10:00:00Z"), ZoneOffset.UTC);
Clock night = Clock.fixed(Instant.parse("2026-10-26T23:30:00Z"), ZoneOffset.UTC);
p("--- time: the same code with an injected Clock ---");
p("acceptsOrders() at 10:00 UTC : %s", new OrderService(gw, morning).acceptsOrders());
p("acceptsOrders() at 23:30 UTC : %s", new OrderService(gw, night).acceptsOrders());
assertTrue(forward.failures.isEmpty());
assertEquals(1, reverse.failures.size());
assertTrue(new OrderService(gw, morning).acceptsOrders());
assertFalse(new OrderService(gw, night).acceptsOrders());
}
@Test
void q30_repeatedTestFailureThreshold() {
q(30, "repeated-test-failure-threshold");
Run run = Run.of(FlakyFixtures.Intermittent.class);
run.events.forEach(e -> p("%s", e));
long failed = run.events.stream().filter(e -> e.endsWith("FAILED")).count();
long skipped = run.events.stream().filter(e -> e.contains("SKIPPED")).count();
p("failed=%d skipped=%d", failed, skipped);
assertTrue(failed >= 2);
}
}
@@ -0,0 +1,41 @@
package lab.jm;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
/**
* Base class for the answer tests: each test calls q(n, slug), records lines with p(...),
* and the transcript is written to output/qNN-slug.txt after the test. The same test asserts
* the facts it prints, so an answer in the article that stops being true fails the build.
*/
abstract class Lab {
private final List<String> lines = new ArrayList<>();
private String file;
void q(int n, String slug) {
file = String.format("q%02d-%s.txt", n, slug);
}
void p(String format, Object... args) {
lines.add(args.length == 0 ? format : String.format(format, args));
}
@AfterEach
void writeTranscript() {
if (file == null) {
return;
}
try {
Path dir = Path.of("output");
Files.createDirectories(dir);
Files.writeString(dir.resolve(file), String.join("\n", lines) + "\n");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
@@ -0,0 +1,75 @@
package lab.jm;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
/** Runs a fixture class through the real JUnit Launcher and records what happened to each test. */
final class Run {
final List<String> events = new ArrayList<>();
final List<Throwable> failures = new ArrayList<>();
private TestPlan plan;
/** For dynamic and templated tests, prefix the invocation with the method it belongs to. */
private String label(TestIdentifier id) {
return plan.getParent(id)
.filter(p -> p.getSource().orElse(null) instanceof org.junit.platform.engine.support.descriptor.MethodSource)
.map(p -> p.getDisplayName() + " / ")
.orElse("") + id.getDisplayName();
}
static Run of(Class<?> fixture) {
return of(fixture, Map.of(), null);
}
static Run of(Class<?> fixture, Map<String, String> params) {
return of(fixture, params, null);
}
static Run of(Class<?> fixture, Map<String, String> params,
org.junit.platform.engine.Filter<?> filter) {
Run run = new Run();
LauncherDiscoveryRequestBuilder b = LauncherDiscoveryRequestBuilder.request()
.selectors(selectClass(fixture));
params.forEach(b::configurationParameter);
if (filter != null) {
b.filters(filter);
}
LauncherDiscoveryRequest request = b.build();
Launcher launcher = LauncherFactory.create();
launcher.execute(request, new TestExecutionListener() {
@Override
public void testPlanExecutionStarted(TestPlan testPlan) {
run.plan = testPlan;
}
@Override
public void executionSkipped(TestIdentifier id, String reason) {
if (id.isTest()) {
run.events.add(run.label(id) + " -> SKIPPED (" + reason + ")");
}
}
@Override
public void executionFinished(TestIdentifier id, TestExecutionResult result) {
result.getThrowable().ifPresent(run.failures::add);
if (id.isTest()) {
run.events.add(run.label(id) + " -> " + result.getStatus());
} else if (result.getStatus() != TestExecutionResult.Status.SUCCESSFUL) {
run.events.add("[container " + id.getDisplayName() + "] -> " + result.getStatus());
}
}
});
return run;
}
}
@@ -0,0 +1,117 @@
package lab.jm.fixtures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.RegisterExtension;
public final class ExtensionFixtures {
private ExtensionFixtures() {
}
/** Logs every callback it receives, tagged with a label. */
public static class Tracer implements BeforeAllCallback, BeforeEachCallback,
BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterEachCallback, AfterAllCallback {
private final String label;
public Tracer() {
this("declared");
}
public Tracer(String label) {
this.label = label;
}
@Override
public void beforeAll(ExtensionContext c) {
Log.add(label + ": beforeAll");
}
@Override
public void beforeEach(ExtensionContext c) {
Log.add(label + ": beforeEach");
}
@Override
public void beforeTestExecution(ExtensionContext c) {
Log.add(label + ": beforeTestExecution");
}
@Override
public void afterTestExecution(ExtensionContext c) {
Log.add(label + ": afterTestExecution");
}
@Override
public void afterEach(ExtensionContext c) {
Log.add(label + ": afterEach");
}
@Override
public void afterAll(ExtensionContext c) {
Log.add(label + ": afterAll");
}
}
/** One declarative extension, to show the callback order around a test and its @BeforeEach method. */
@ExtendWith(Tracer.class)
public static class CallbackOrder {
@BeforeEach
void beforeEachMethod() {
Log.add("test class: @BeforeEach method");
}
@Test
void theTest() {
Log.add("test class: the test body");
}
}
/** Declarative (@ExtendWith) and programmatic (@RegisterExtension) side by side. */
@ExtendWith(Tracer.class)
public static class Registration {
@RegisterExtension
static final Tracer PROGRAMMATIC = new Tracer("programmatic(static field)");
@Test
void theTest() {
Log.add("test body");
}
}
/** A value the custom resolver hands to tests. */
public record Tenant(String name) {
}
/** Resolves Tenant parameters, in constructors and in test methods alike. */
public static class TenantResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext p, ExtensionContext c) {
return p.getParameter().getType() == Tenant.class;
}
@Override
public Object resolveParameter(ParameterContext p, ExtensionContext c) {
return new Tenant("tenant-for-" + c.getRequiredTestMethod().getName());
}
}
@ExtendWith(TenantResolver.class)
public static class Injection {
@Test
void resolvesCustomAndBuiltIn(Tenant tenant, TestInfo info) {
Log.add("custom parameter : " + tenant);
Log.add("built-in TestInfo: displayName=" + info.getDisplayName());
}
}
}
@@ -0,0 +1,54 @@
package lab.jm.fixtures;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.MethodOrdererContext;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.Test;
public final class FlakyFixtures {
private FlakyFixtures() {
}
/** Two tests that secretly share state: the second only passes if the first ran before it. */
public static class SharedState {
public static final List<String> REGISTRY = new ArrayList<>();
@Test
void a_registersUser() {
REGISTRY.add("ada");
}
@Test
void b_assumesUserExists() {
assertEquals(1, REGISTRY.size(), "registry should contain the user registered by another test");
}
}
/** Orders methods in reverse alphabetical order, to expose hidden order dependence. */
public static class ReverseOrder implements MethodOrderer {
@Override
public void orderMethods(MethodOrdererContext context) {
context.getMethodDescriptors().sort(
(x, y) -> y.getMethod().getName().compareTo(x.getMethod().getName()));
}
}
/** A test that fails on exactly one repetition, like an intermittent bug that shows up now and then. */
public static class Intermittent {
@RepeatedTest(value = 6, failureThreshold = 1)
void failsOnThirdRepetition(RepetitionInfo info) {
assertTrue(info.getCurrentRepetition() != 3, "intermittent failure on repetition 3");
}
@RepeatedTest(value = 6)
void withoutThreshold(RepetitionInfo info) {
assertTrue(info.getCurrentRepetition() != 3, "intermittent failure on repetition 3");
}
}
}
@@ -0,0 +1,156 @@
package lab.jm.fixtures;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
/**
* Test classes that are launched on purpose by the answer tests, never by Surefire
* (the class name does not match Surefire's default *Test / *Tests patterns).
*/
public final class LifecycleFixtures {
private LifecycleFixtures() {
}
/** Default lifecycle: PER_METHOD. */
public static class PerMethod {
public static int instances;
final int id = ++instances;
@BeforeAll
static void beforeAll() {
Log.add("@BeforeAll (static)");
}
@BeforeEach
void beforeEach() {
Log.add("@BeforeEach on instance #" + id);
}
@Test
void first() {
Log.add(" test first() on instance #" + id);
}
@Test
void second() {
Log.add(" test second() on instance #" + id);
}
@AfterEach
void afterEach() {
Log.add("@AfterEach on instance #" + id);
}
@AfterAll
static void afterAll() {
Log.add("@AfterAll (static)");
}
}
/** PER_CLASS: one instance, so @BeforeAll can be non-static and fields survive between tests. */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public static class PerClass {
public static int instances;
final int id = ++instances;
int counter;
@BeforeAll
void beforeAll() {
Log.add("@BeforeAll (non-static) on instance #" + id);
}
@Test
void first() {
counter++;
Log.add("first(): instance #" + id + ", counter=" + counter);
}
@Test
void second() {
counter++;
Log.add("second(): instance #" + id + ", counter=" + counter);
}
}
/** Outer and @Nested lifecycle callbacks. */
public static class Outer {
@BeforeEach
void outerBefore() {
Log.add("outer @BeforeEach");
}
@AfterEach
void outerAfter() {
Log.add("outer @AfterEach");
}
@Test
void outerTest() {
Log.add(" outerTest()");
}
@Nested
class Inner {
@BeforeEach
void innerBefore() {
Log.add("inner @BeforeEach");
}
@AfterEach
void innerAfter() {
Log.add("inner @AfterEach");
}
@Test
void innerTest() {
Log.add(" innerTest()");
}
}
}
/** Three methods whose declaration order is not alphabetical. */
public static class Ordering {
@Test
void zebra() {
}
@Test
void apple() {
}
@Test
void mango() {
}
@Test
void banana() {
}
}
/** Assertion failure versus failed assumption. */
public static class Outcomes {
@Test
void passes() {
assertEquals(2, 1 + 1);
}
@Test
void assertionFails() {
assertEquals(3, 1 + 1);
}
@Test
void assumptionFails() {
assumeTrue(false, "not on this machine");
throw new AssertionError("never reached");
}
}
}
@@ -0,0 +1,25 @@
package lab.jm.fixtures;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Shared recorder the fixtures write to while a test launches them through the Launcher API. */
public final class Log {
private static final List<String> LINES = Collections.synchronizedList(new ArrayList<>());
private Log() {
}
public static void add(String line) {
LINES.add(line);
}
public static void clear() {
LINES.clear();
}
public static List<String> lines() {
return new ArrayList<>(LINES);
}
}
@@ -0,0 +1,41 @@
package lab.jm.fixtures;
import static org.mockito.Mockito.when;
import lab.jm.PaymentGateway;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
public final class MockitoFixtures {
private MockitoFixtures() {
}
/** MockitoExtension defaults to strict stubs: a stubbing the test never uses fails the test. */
@ExtendWith(MockitoExtension.class)
public static class StrictByDefault {
@Mock
PaymentGateway gateway;
@Test
void unusedStub() {
when(gateway.lookup("42")).thenReturn("Ada");
}
}
/** Same test with lenient strictness: no failure. */
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public static class Lenient {
@Mock
PaymentGateway gateway;
@Test
void unusedStub() {
when(gateway.lookup("42")).thenReturn("Ada");
}
}
}
@@ -0,0 +1,139 @@
package lab.jm.fixtures;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
public final class ParamFixtures {
private ParamFixtures() {
}
public enum Level { LOW, HIGH }
/** One example of each common source; the answer test records every invocation's display name. */
public static class Sources {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void valueSource(int n) {
assertTrue(n > 0);
}
@ParameterizedTest
@CsvSource({"apple, 1", "banana, 2"})
void csvSource(String fruit, int qty) {
assertTrue(qty > 0);
}
@ParameterizedTest
@MethodSource("pairs")
void methodSource(String key, int value) {
assertTrue(value > 0);
}
static Stream<Arguments> pairs() {
return Stream.of(Arguments.of("x", 10), Arguments.of("y", 20));
}
@ParameterizedTest
@EnumSource(Level.class)
void enumSource(Level level) {
assertTrue(level != null);
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" "})
void nullAndEmpty(String s) {
assertTrue(s == null || s.isBlank());
}
}
/** Shows how CSV text becomes null, empty string or a literal. */
public static class CsvConversion {
@ParameterizedTest(name = "[{index}] a={0} b={1}")
@CsvSource(value = {"x,", ",y", "'',z", "NIL,w", "'a,b',q"}, nullValues = "NIL")
void csv(String a, String b) {
Log.add("a=" + show(a) + " b=" + show(b));
}
private static String show(String s) {
return s == null ? "null" : "\"" + s + "\"";
}
}
/** A dynamic-test factory and a @BeforeEach that shows how often it runs. */
public static class Dynamic {
static int beforeEachRuns;
@BeforeEach
void beforeEach() {
beforeEachRuns++;
Log.add("@BeforeEach ran (" + beforeEachRuns + " time so far)");
}
@TestFactory
List<DynamicTest> squares() {
return List.of(1, 2, 3).stream()
.map(n -> DynamicTest.dynamicTest("square of " + n,
() -> assertTrue(n * n >= n)))
.toList();
}
}
/** Disabled, conditional and tagged tests. */
public static class Conditions {
@Test
@Disabled("waiting for the fix in ticket 1234")
void disabled() {
}
@Test
@EnabledIfSystemProperty(named = "lab.run.slow", matches = "true")
void onlyWithProperty() {
}
@Test
@Tag("fast")
void fast() {
}
@Test
@Tag("slow")
void slow() {
}
}
/** @TempDir: created before the test, deleted after it. */
public static class TempDirs {
public static volatile Path seen;
@TempDir
Path dir;
@Test
void writesAFile() throws Exception {
Files.writeString(dir.resolve("a.txt"), "hello");
seen = dir;
Log.add("inside the test : dir exists = " + Files.exists(dir)
+ ", files = " + Files.list(dir).count());
}
}
}