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:
@@ -0,0 +1,7 @@
|
||||
default MockMaker : org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker
|
||||
PriceCalculator is final : true
|
||||
mock class name : lab.mockito.PriceCalculator
|
||||
mock class == real class : true
|
||||
stubbed final method : applyDiscount(1000, 10) = 42
|
||||
unstubbed final method : applyDiscount(1000, 20) = 0
|
||||
mocked a record : true
|
||||
@@ -0,0 +1,4 @@
|
||||
before scope : id starts with ORD- : true
|
||||
inside scope : place("alice") = ORD-FIXED@alice
|
||||
inside scope : unstubbed Ids.nowMillis() = 0 (default answer, not the clock)
|
||||
after scope : id starts with ORD- and is not FIXED : true
|
||||
@@ -0,0 +1,2 @@
|
||||
test thread sees : ORD-FIXED
|
||||
other thread sees : real id, not FIXED = true
|
||||
@@ -0,0 +1,4 @@
|
||||
MockitoException:
|
||||
For lab.mockito.Ids, static mocking is already registered in the current thread
|
||||
|
||||
To create a new mock, the existing static mock registration must be deregistered
|
||||
@@ -0,0 +1,2 @@
|
||||
newOrderId() = ORD-FIXED
|
||||
nowMillis() is the real clock (> 0) : true
|
||||
@@ -0,0 +1,2 @@
|
||||
MockitoException:
|
||||
It is not possible to mock static methods of java.lang.System to avoid interfering with class loading what leads to infinite loops
|
||||
@@ -0,0 +1,6 @@
|
||||
no interception : real SMTP connection attempted to smtp.example.com
|
||||
constructor args : [smtp.example.com]
|
||||
construction # : 1
|
||||
welcome("bob") : true
|
||||
instances built : 1
|
||||
verified : send("[email protected]", "Welcome, bob")
|
||||
@@ -0,0 +1,3 @@
|
||||
charged amounts : [900, 500]
|
||||
last captured : 500
|
||||
audit details : [acc-9:900, acc-7:500]
|
||||
@@ -0,0 +1,11 @@
|
||||
org.mockito.exceptions.base.MockitoException:
|
||||
No argument value was captured!
|
||||
You might have forgotten to use argument.capture() in verify()...
|
||||
...or you used capture() in stubbing but stubbed method was not called.
|
||||
Be aware that it is recommended to use capture() only with verify()
|
||||
|
||||
Examples of correct argument capturing:
|
||||
ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
|
||||
verify(mock).doSomething(argument.capture());
|
||||
assertEquals("John", argument.getValue().getName());
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
receipt approved : true
|
||||
then(gateway).should() : charge("acc-1", 900) ok
|
||||
then(audit).should() : record("CHECKOUT", "acc-1:900") ok
|
||||
shouldHaveNoMoreInteractions on audit : ok
|
||||
InOrder gateway.charge -> audit.record : ok
|
||||
@@ -0,0 +1,3 @@
|
||||
exception : gateway unavailable
|
||||
gateway.charge never called : ok
|
||||
audit recorded CHECKOUT_REJECTED : ok
|
||||
@@ -0,0 +1,6 @@
|
||||
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.mockito.StrictnessTest.unusedStubIsReportedWhenTheSessionEnds(StrictnessTest.java:33)
|
||||
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
|
||||
@@ -0,0 +1,16 @@
|
||||
PotentialStubbingProblem:
|
||||
Strict stubbing argument mismatch. Please check:
|
||||
- this invocation of 'charge' method:
|
||||
paymentGateway.charge("acc-1", 1000L);
|
||||
-> at lab.mockito.CheckoutService.checkout(CheckoutService.java:20)
|
||||
- has following stubbing(s) with different arguments:
|
||||
1. paymentGateway.charge("acc-1", 900L); stubbed with: [Returns: Receipt[id=r, account=acc-1, cents=900, approved=true]]
|
||||
-> at lab.mockito.StrictnessTest.sameMethodDifferentArgumentsIsAPotentialStubbingProblem(StrictnessTest.java:48)
|
||||
Typically, stubbing argument mismatch indicates user mistake when writing tests.
|
||||
Mockito fails early so that you can debug potential problem easily.
|
||||
However, there are legit scenarios when this exception generates false negative signal:
|
||||
- stubbing the same method multiple times using 'given().will()' or 'when().then()' API
|
||||
Please use 'will().given()' or 'doReturn().when()' API for stubbing.
|
||||
- stubbed method is intentionally invoked with different arguments by code under test
|
||||
Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).
|
||||
For more information see javadoc for PotentialStubbingProblem class.
|
||||
@@ -0,0 +1 @@
|
||||
charge("acc-1", 1000) from the test file returns : null (no PotentialStubbingProblem)
|
||||
@@ -0,0 +1,2 @@
|
||||
lenient stub, mismatching call returns : null
|
||||
finishMocking with unused lenient stubs : no exception
|
||||
@@ -0,0 +1 @@
|
||||
mismatching call on a plain mock returns : null (no strictness error)
|
||||
@@ -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 (<local-repo>/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,63 @@
|
||||
<?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.testing</groupId>
|
||||
<artifactId>mockito-lab</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 inline mock maker attaches a Java agent. Loading it at startup (the default
|
||||
argLine below) avoids the JDK warning about dynamic agent loading. -->
|
||||
<mockito.agent>${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar</mockito.agent>
|
||||
<surefire.argLine>-javaagent:${mockito.agent} -Xshare:off</surefire.argLine>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<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.5.6</version>
|
||||
<configuration>
|
||||
<argLine>${surefire.argLine}</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<!-- Run with -Pno-agent to see the JDK warning Mockito triggers when it self-attaches. -->
|
||||
<profile>
|
||||
<id>no-agent</id>
|
||||
<properties>
|
||||
<surefire.argLine>-Xshare:off</surefire.argLine>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates every file in mockito/output/.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
unset JAVA_TOOL_OPTIONS
|
||||
|
||||
# 1. The whole suite with Mockito loaded as a -javaagent (the recommended setup).
|
||||
mvn -B -q clean test
|
||||
|
||||
# 2. The same single test WITHOUT the agent, to capture what a JDK 21+ prints when Mockito self-attaches.
|
||||
mvn -B -Pno-agent test -Dtest=MockMakerTest 2>&1 \
|
||||
| grep -E '^(Mockito is currently|WARNING: (A Java agent|If a serviceability|Dynamic loading))' \
|
||||
| sed -E 's#\(/[^)]*/byte-buddy-agent-([0-9.]+)\.jar\)#(<local-repo>/byte-buddy-agent-\1.jar)#' \
|
||||
> output/14-dynamic-agent-warning.txt
|
||||
echo "done: $(ls output | wc -l) files in output/"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user