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
@@ -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());
}
}
}