mockito: Mockito 5 lab (inline mock maker, static/construction mocking, captors, BDD, strict stubs)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cfo2K4u4rF18QsPXPFnytf
This commit is contained in:
2026-10-24 17:19:45 +00:00
co-authored by Claude Sonnet 5.5
commit 8fc8bc934f
38 changed files with 689 additions and 0 deletions
@@ -0,0 +1,5 @@
package lab.mockito;
public interface AuditLog {
void record(String event, String detail);
}
@@ -0,0 +1,24 @@
package lab.mockito;
public class CheckoutService {
private final PaymentGateway gateway;
private final AuditLog audit;
private final PriceCalculator calculator;
public CheckoutService(PaymentGateway gateway, AuditLog audit, PriceCalculator calculator) {
this.gateway = gateway;
this.audit = audit;
this.calculator = calculator;
}
public Receipt checkout(String account, long cents, int discountPercent) {
if (!gateway.isAvailable()) {
audit.record("CHECKOUT_REJECTED", account);
throw new IllegalStateException("gateway unavailable");
}
long payable = calculator.applyDiscount(cents, discountPercent);
Receipt receipt = gateway.charge(account, payable);
audit.record("CHECKOUT", account + ":" + payable);
return receipt;
}
}
@@ -0,0 +1,16 @@
package lab.mockito;
/** Pretend this talks to an SMTP server. Real sending is not what the tests are about. */
public class EmailClient {
public EmailClient() {
throw new IllegalStateException("real SMTP connection attempted");
}
public EmailClient(String host) {
throw new IllegalStateException("real SMTP connection attempted to " + host);
}
public boolean send(String to, String body) {
throw new IllegalStateException("real email sent to " + to);
}
}
@@ -0,0 +1,16 @@
package lab.mockito;
import java.util.UUID;
/** Static helpers: hard to substitute without mockStatic. */
public final class Ids {
private Ids() {}
public static String newOrderId() {
return "ORD-" + UUID.randomUUID().toString().substring(0, 8);
}
public static long nowMillis() {
return System.currentTimeMillis();
}
}
@@ -0,0 +1,9 @@
package lab.mockito;
/** Builds its own EmailClient with `new` - no injection point, so only mockConstruction can intercept it. */
public class NotificationService {
public boolean welcome(String user) {
EmailClient client = new EmailClient("smtp.example.com");
return client.send(user + "@example.com", "Welcome, " + user);
}
}
@@ -0,0 +1,8 @@
package lab.mockito;
public class OrderService {
public String place(String customer) {
String id = Ids.newOrderId();
return id + "@" + customer;
}
}
@@ -0,0 +1,7 @@
package lab.mockito;
public interface PaymentGateway {
Receipt charge(String account, long cents);
boolean isAvailable();
}
@@ -0,0 +1,8 @@
package lab.mockito;
/** A final class with a final method: the classic "Mockito 1/2 could not mock this" case. */
public final class PriceCalculator {
public final long applyDiscount(long cents, int percent) {
return cents - (cents * percent / 100);
}
}
@@ -0,0 +1,3 @@
package lab.mockito;
public record Receipt(String id, String account, long cents, boolean approved) {}
@@ -0,0 +1,62 @@
package lab.mockito;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.BDDMockito.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class BddStyleTest {
@Mock PaymentGateway gateway;
@Mock AuditLog audit;
@Mock PriceCalculator calculator;
@Test
void givenWhenThen() {
Transcript t = new Transcript("08-bdd-style.txt");
// given
given(gateway.isAvailable()).willReturn(true);
given(calculator.applyDiscount(1000, 10)).willReturn(900L);
given(gateway.charge("acc-1", 900)).willReturn(new Receipt("r-1", "acc-1", 900, true));
CheckoutService svc = new CheckoutService(gateway, audit, calculator);
// when
Receipt receipt = svc.checkout("acc-1", 1000, 10);
// then
assertTrue(receipt.approved());
then(gateway).should().charge("acc-1", 900);
then(audit).should().record("CHECKOUT", "acc-1:900");
then(audit).shouldHaveNoMoreInteractions();
InOrder order = Mockito.inOrder(gateway, audit);
order.verify(gateway).charge("acc-1", 900);
order.verify(audit).record("CHECKOUT", "acc-1:900");
t.line("receipt approved : %s", receipt.approved());
t.line("then(gateway).should() : charge(\"acc-1\", 900) ok");
t.line("then(audit).should() : record(\"CHECKOUT\", \"acc-1:900\") ok");
t.line("shouldHaveNoMoreInteractions on audit : ok");
t.line("InOrder gateway.charge -> audit.record : ok");
t.write();
}
@Test
void willThrowAndNeverCalled() {
Transcript t = new Transcript("09-bdd-throw-and-never.txt");
given(gateway.isAvailable()).willReturn(false);
CheckoutService svc = new CheckoutService(gateway, audit, calculator);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> svc.checkout("acc-2", 1000, 10));
then(gateway).should(never()).charge(anyString(), anyLong());
then(audit).should().record("CHECKOUT_REJECTED", "acc-2");
t.line("exception : %s", e.getMessage());
t.line("gateway.charge never called : ok");
t.line("audit recorded CHECKOUT_REJECTED : ok");
t.write();
}
}
@@ -0,0 +1,51 @@
package lab.mockito;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class CaptorTest {
@Mock PaymentGateway gateway;
@Mock AuditLog audit;
@Captor ArgumentCaptor<String> detail;
@Captor ArgumentCaptor<Long> cents;
@Test
void captureWhatTheCodeActuallySent() {
Transcript t = new Transcript("06-argument-captor.txt");
when(gateway.isAvailable()).thenReturn(true);
when(gateway.charge(anyString(), anyLong())).thenReturn(new Receipt("r-1", "acc-9", 900, true));
CheckoutService svc = new CheckoutService(gateway, audit, new PriceCalculator());
svc.checkout("acc-9", 1000, 10);
svc.checkout("acc-7", 500, 0);
verify(gateway, times(2)).charge(anyString(), cents.capture());
t.line("charged amounts : %s", cents.getAllValues());
assertEquals(List.of(900L, 500L), cents.getAllValues());
t.line("last captured : %d", cents.getValue());
verify(audit, times(2)).record(eq("CHECKOUT"), detail.capture());
t.line("audit details : %s", detail.getAllValues());
assertEquals(List.of("acc-9:900", "acc-7:500"), detail.getAllValues());
t.write();
}
@Test
void getValueBeforeAnyCaptureFails() {
Transcript t = new Transcript("07-captor-empty.txt");
ArgumentCaptor<String> empty = ArgumentCaptor.forClass(String.class);
Exception e = assertThrows(Exception.class, empty::getValue);
t.line("%s: %s", e.getClass().getName(), e.getMessage());
t.write();
}
}
@@ -0,0 +1,34 @@
package lab.mockito;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;
class ConstructionMockingTest {
@Test
void mockConstructionInterceptsNewInsideTheClassUnderTest() {
Transcript t = new Transcript("05-mock-construction.txt");
// Without mockConstruction the real constructor throws - that is the point of the demo.
IllegalStateException real = assertThrows(IllegalStateException.class, () -> new NotificationService().welcome("bob"));
t.line("no interception : %s", real.getMessage());
try (MockedConstruction<EmailClient> constructed = mockConstruction(EmailClient.class,
(mock, context) -> {
t.line("constructor args : %s", context.arguments());
t.line("construction # : %d", context.getCount());
when(mock.send(anyString(), anyString())).thenReturn(true);
})) {
boolean sent = new NotificationService().welcome("bob");
t.line("welcome(\"bob\") : %s", sent);
t.line("instances built : %d", constructed.constructed().size());
assertTrue(sent);
assertEquals(1, constructed.constructed().size());
verify(constructed.constructed().get(0)).send("[email protected]", "Welcome, bob");
t.line("verified : send(\"[email protected]\", \"Welcome, bob\")");
}
t.write();
}
}
@@ -0,0 +1,34 @@
package lab.mockito;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.plugins.MockMaker;
class MockMakerTest {
@Test
void inlineMockMakerIsTheDefaultAndMocksFinalTypes() {
Transcript t = new Transcript("01-mock-maker.txt");
MockMaker maker = Mockito.framework().getPlugins().getDefaultPlugin(MockMaker.class);
t.line("default MockMaker : %s", maker.getClass().getName());
assertTrue(maker.getClass().getSimpleName().contains("Inline"));
PriceCalculator calc = mock(PriceCalculator.class);
t.line("PriceCalculator is final : %s", java.lang.reflect.Modifier.isFinal(PriceCalculator.class.getModifiers()));
t.line("mock class name : %s", calc.getClass().getName());
t.line("mock class == real class : %s", calc.getClass() == PriceCalculator.class);
when(calc.applyDiscount(1000, 10)).thenReturn(42L);
t.line("stubbed final method : applyDiscount(1000, 10) = %d", calc.applyDiscount(1000, 10));
t.line("unstubbed final method : applyDiscount(1000, 20) = %d", calc.applyDiscount(1000, 20));
assertEquals(42L, calc.applyDiscount(1000, 10));
assertEquals(0L, calc.applyDiscount(1000, 20));
Receipt r = mock(Receipt.class);
t.line("mocked a record : %s", r.getClass() == Receipt.class);
t.write();
}
}
@@ -0,0 +1,83 @@
package lab.mockito;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.exceptions.base.MockitoException;
class StaticMockingTest {
@Test
void mockStaticReplacesTheMethodOnlyInsideTheTryBlock() {
Transcript t = new Transcript("02-mock-static.txt");
String before = Ids.newOrderId();
t.line("before scope : id starts with ORD- : %s", before.startsWith("ORD-"));
try (MockedStatic<Ids> ids = mockStatic(Ids.class)) {
ids.when(Ids::newOrderId).thenReturn("ORD-FIXED");
String placed = new OrderService().place("alice");
t.line("inside scope : place(\"alice\") = %s", placed);
assertEquals("ORD-FIXED@alice", placed);
ids.verify(Ids::newOrderId);
ids.verify(Ids::newOrderId, times(1));
t.line("inside scope : unstubbed Ids.nowMillis() = %d (default answer, not the clock)", Ids.nowMillis());
assertEquals(0L, Ids.nowMillis());
}
String after = Ids.newOrderId();
t.line("after scope : id starts with ORD- and is not FIXED : %s", after.startsWith("ORD-") && !after.equals("ORD-FIXED"));
assertNotEquals("ORD-FIXED", after);
t.write();
}
@Test
void staticMockIsThreadLocal() throws Exception {
Transcript t = new Transcript("03-static-thread-local.txt");
AtomicReference<String> seenByOtherThread = new AtomicReference<>();
try (MockedStatic<Ids> ids = mockStatic(Ids.class)) {
ids.when(Ids::newOrderId).thenReturn("ORD-FIXED");
Thread other = new Thread(() -> seenByOtherThread.set(Ids.newOrderId()));
other.start();
other.join();
t.line("test thread sees : %s", Ids.newOrderId());
t.line("other thread sees : real id, not FIXED = %s", !"ORD-FIXED".equals(seenByOtherThread.get()));
}
assertNotEquals("ORD-FIXED", seenByOtherThread.get());
t.write();
}
@Test
void registeringTheSameStaticTwiceOnOneThreadFails() {
Transcript t = new Transcript("04-static-already-registered.txt");
try (MockedStatic<Ids> first = mockStatic(Ids.class)) {
MockitoException e = assertThrows(MockitoException.class, () -> mockStatic(Ids.class));
t.block("MockitoException:", e.getMessage());
}
t.write();
}
@Test
void callsRealMethodsKeepsTheRestOfTheClassReal() {
Transcript t = new Transcript("04b-static-calls-real-methods.txt");
try (MockedStatic<Ids> ids = mockStatic(Ids.class, CALLS_REAL_METHODS)) {
ids.when(Ids::newOrderId).thenReturn("ORD-FIXED");
long now = Ids.nowMillis();
t.line("newOrderId() = %s", Ids.newOrderId());
t.line("nowMillis() is the real clock (> 0) : %s", now > 0);
assertTrue(now > 0);
}
t.write();
}
@Test
void coreJdkClassesCannotBeStaticallyMocked() {
Transcript t = new Transcript("04c-static-mock-system.txt");
MockitoException e = assertThrows(MockitoException.class, () -> mockStatic(System.class));
t.block("MockitoException:", e.getMessage());
t.write();
}
}
@@ -0,0 +1,103 @@
package lab.mockito;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoSession;
import org.mockito.exceptions.misusing.PotentialStubbingProblem;
import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
import org.mockito.quality.Strictness;
/** Uses the MockitoSession API so the strictness failures can be caught and recorded. */
class StrictnessTest {
private static void finish(MockitoSession session) {
try {
session.finishMocking();
} catch (UnnecessaryStubbingException ignored) {
// the demo stubs are deliberately left unused in some tests
}
}
private MockitoSession strict() {
return org.mockito.Mockito.mockitoSession().strictness(Strictness.STRICT_STUBS).startMocking();
}
@Test
void unusedStubIsReportedWhenTheSessionEnds() {
Transcript t = new Transcript("10-unnecessary-stubbing.txt");
MockitoSession session = strict();
PaymentGateway gateway = mock(PaymentGateway.class);
when(gateway.isAvailable()).thenReturn(true); // used below
when(gateway.charge("acc-1", 100)).thenReturn(null); // never used
gateway.isAvailable();
UnnecessaryStubbingException e = assertThrows(UnnecessaryStubbingException.class, session::finishMocking);
t.block("UnnecessaryStubbingException:", e.getMessage());
t.write();
}
@Test
void sameMethodDifferentArgumentsIsAPotentialStubbingProblem() {
Transcript t = new Transcript("11-potential-stubbing-problem.txt");
MockitoSession session = strict();
try {
PaymentGateway gateway = mock(PaymentGateway.class);
when(gateway.isAvailable()).thenReturn(true);
when(gateway.charge("acc-1", 900)).thenReturn(new Receipt("r", "acc-1", 900, true));
CheckoutService svc = new CheckoutService(gateway, mock(AuditLog.class), new PriceCalculator());
// 1000 cents with 0% discount reaches charge("acc-1", 1000) inside CheckoutService.java,
// a different source file from this test: Mockito flags the argument mismatch.
PotentialStubbingProblem e = assertThrows(PotentialStubbingProblem.class, () -> svc.checkout("acc-1", 1000, 0));
t.block("PotentialStubbingProblem:", e.getMessage());
} finally {
finish(session);
}
t.write();
}
@Test
void sameFileMismatchIsNotFlagged() {
Transcript t = new Transcript("11b-same-file-mismatch-not-flagged.txt");
MockitoSession session = strict();
try {
PaymentGateway gateway = mock(PaymentGateway.class);
when(gateway.charge("acc-1", 900)).thenReturn(new Receipt("r", "acc-1", 900, true));
// Same call-shape as the test above, but made from this very file.
Receipt r = assertDoesNotThrow(() -> gateway.charge("acc-1", 1000));
t.line("charge(\"acc-1\", 1000) from the test file returns : %s (no PotentialStubbingProblem)", r);
assertNull(r);
} finally {
finish(session);
}
t.write();
}
@Test
void lenientStubsAreExemptAndStrictnessCanBeLoosenedPerStub() {
Transcript t = new Transcript("12-lenient.txt");
MockitoSession session = strict();
PaymentGateway gateway = mock(PaymentGateway.class);
lenient().when(gateway.charge("acc-1", 100)).thenReturn(new Receipt("r", "acc-1", 100, true));
lenient().when(gateway.isAvailable()).thenReturn(true);
Receipt other = gateway.charge("acc-1", 999); // arg mismatch, but the stub is lenient
t.line("lenient stub, mismatching call returns : %s", other);
assertNull(other);
session.finishMocking(); // unused lenient stub: no exception
t.line("finishMocking with unused lenient stubs : no exception");
t.write();
}
@Test
void plainMockWithoutSessionIsLenient() {
Transcript t = new Transcript("13-no-session-no-checks.txt");
PaymentGateway gateway = mock(PaymentGateway.class);
when(gateway.charge("acc-1", 100)).thenReturn(new Receipt("r", "acc-1", 100, true));
Receipt other = gateway.charge("acc-1", 999);
t.line("mismatching call on a plain mock returns : %s (no strictness error)", other);
assertNull(other);
t.write();
}
}
@@ -0,0 +1,45 @@
package lab.mockito;
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;
/**
* Collects lines during a test and writes them to output/NN-name.txt.
* The same test asserts the same facts, so every figure in the blog post is an assertion.
*/
final class Transcript {
private final String file;
private final List<String> lines = new ArrayList<>();
Transcript(String file) {
this.file = file;
}
Transcript line(String format, Object... args) {
lines.add(args.length == 0 ? format : String.format(format, args));
return this;
}
/** Message text with the volatile first-line class prefix kept, trailing whitespace trimmed. */
Transcript block(String label, String text) {
lines.add(label);
for (String l : text.strip().split("\n")) {
lines.add(" " + l.stripTrailing());
}
return this;
}
void write() {
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);
}
}
}