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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
inside the test : dir exists = true, files = 1
|
||||||
|
after the test finished : dir exists = false
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
Executable
+20
@@ -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"
|
||||||
Executable
+13
@@ -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"
|
||||||
Executable
+11
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user