From 715036b64007706d6efa55b5193b98b4de1f2192 Mon Sep 17 00:00:00 2001 From: asmhatre Date: Mon, 26 Oct 2026 01:52:44 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01CifR2BE7PBvRy5hT8TS9oz --- README.md | 1 + junit-mockito/README.md | 21 ++ .../output/q01-platform-jupiter-vintage.txt | 6 + junit-mockito/output/q02-jar-comparison.txt | 2 + .../q02-junit6-baseline-and-removals.txt | 3 + junit-mockito/output/q03-lifecycle-order.txt | 8 + .../output/q04-per-class-lifecycle.txt | 3 + junit-mockito/output/q05-nested-lifecycle.txt | 8 + junit-mockito/output/q06-method-order.txt | 4 + .../q07-assert-all-and-assert-throws.txt | 5 + .../output/q08-timeout-vs-preemptive.txt | 4 + .../output/q09-assertion-vs-assumption.txt | 3 + .../output/q10-extension-callback-order.txt | 8 + .../q11-extend-with-vs-register-extension.txt | 13 ++ .../output/q12-parameter-resolver.txt | 2 + .../output/q13-parameterized-sources.txt | 12 ++ .../output/q14-csv-null-and-empty.txt | 11 + junit-mockito/output/q15-dynamic-tests.txt | 4 + .../output/q16-disabled-conditional-tags.txt | 7 + junit-mockito/output/q17-temp-dir.txt | 2 + junit-mockito/output/q18-mock-vs-spy.txt | 2 + .../output/q19-when-vs-doreturn-on-spy.txt | 3 + .../output/q20-verify-times-never-inorder.txt | 14 ++ .../output/q21-matchers-and-captor.txt | 5 + junit-mockito/output/q22-strict-stubs.txt | 11 + junit-mockito/output/q23-stubbing-styles.txt | 4 + .../output/q24-final-classes-mock-maker.txt | 5 + junit-mockito/output/q25-mock-static.txt | 4 + .../output/q26-mock-construction.txt | 4 + junit-mockito/output/q27-no-agent-warning.txt | 5 + .../output/q28-unstubbed-defaults.txt | 6 + .../output/q29-flaky-order-and-time.txt | 10 + .../q30-repeated-test-failure-threshold.txt | 13 ++ junit-mockito/pom.xml | 68 +++++++ junit-mockito/scripts/compare-5-vs-6.sh | 20 ++ junit-mockito/scripts/no-agent.sh | 13 ++ junit-mockito/scripts/run-all.sh | 11 + .../src/main/java/lab/jm/Counter.java | 16 ++ .../src/main/java/lab/jm/FinalPricer.java | 8 + junit-mockito/src/main/java/lab/jm/Ids.java | 11 + .../src/main/java/lab/jm/OrderService.java | 30 +++ .../src/main/java/lab/jm/PaymentGateway.java | 10 + .../src/main/java/lab/jm/Receipt.java | 18 ++ .../java/lab/jm/A_JUnitFundamentalsTest.java | 171 ++++++++++++++++ .../test/java/lab/jm/B_ExtensionsTest.java | 44 ++++ .../lab/jm/C_ParameterizedAndDynamicTest.java | 73 +++++++ .../test/java/lab/jm/D_MockitoCoreTest.java | 188 ++++++++++++++++++ .../java/lab/jm/E_MockitoAdvancedTest.java | 74 +++++++ .../src/test/java/lab/jm/F_FlakyTest.java | 56 ++++++ junit-mockito/src/test/java/lab/jm/Lab.java | 41 ++++ junit-mockito/src/test/java/lab/jm/Run.java | 75 +++++++ .../lab/jm/fixtures/ExtensionFixtures.java | 117 +++++++++++ .../java/lab/jm/fixtures/FlakyFixtures.java | 54 +++++ .../lab/jm/fixtures/LifecycleFixtures.java | 156 +++++++++++++++ .../src/test/java/lab/jm/fixtures/Log.java | 25 +++ .../java/lab/jm/fixtures/MockitoFixtures.java | 41 ++++ .../java/lab/jm/fixtures/ParamFixtures.java | 139 +++++++++++++ 57 files changed, 1672 insertions(+) create mode 100644 junit-mockito/README.md create mode 100644 junit-mockito/output/q01-platform-jupiter-vintage.txt create mode 100644 junit-mockito/output/q02-jar-comparison.txt create mode 100644 junit-mockito/output/q02-junit6-baseline-and-removals.txt create mode 100644 junit-mockito/output/q03-lifecycle-order.txt create mode 100644 junit-mockito/output/q04-per-class-lifecycle.txt create mode 100644 junit-mockito/output/q05-nested-lifecycle.txt create mode 100644 junit-mockito/output/q06-method-order.txt create mode 100644 junit-mockito/output/q07-assert-all-and-assert-throws.txt create mode 100644 junit-mockito/output/q08-timeout-vs-preemptive.txt create mode 100644 junit-mockito/output/q09-assertion-vs-assumption.txt create mode 100644 junit-mockito/output/q10-extension-callback-order.txt create mode 100644 junit-mockito/output/q11-extend-with-vs-register-extension.txt create mode 100644 junit-mockito/output/q12-parameter-resolver.txt create mode 100644 junit-mockito/output/q13-parameterized-sources.txt create mode 100644 junit-mockito/output/q14-csv-null-and-empty.txt create mode 100644 junit-mockito/output/q15-dynamic-tests.txt create mode 100644 junit-mockito/output/q16-disabled-conditional-tags.txt create mode 100644 junit-mockito/output/q17-temp-dir.txt create mode 100644 junit-mockito/output/q18-mock-vs-spy.txt create mode 100644 junit-mockito/output/q19-when-vs-doreturn-on-spy.txt create mode 100644 junit-mockito/output/q20-verify-times-never-inorder.txt create mode 100644 junit-mockito/output/q21-matchers-and-captor.txt create mode 100644 junit-mockito/output/q22-strict-stubs.txt create mode 100644 junit-mockito/output/q23-stubbing-styles.txt create mode 100644 junit-mockito/output/q24-final-classes-mock-maker.txt create mode 100644 junit-mockito/output/q25-mock-static.txt create mode 100644 junit-mockito/output/q26-mock-construction.txt create mode 100644 junit-mockito/output/q27-no-agent-warning.txt create mode 100644 junit-mockito/output/q28-unstubbed-defaults.txt create mode 100644 junit-mockito/output/q29-flaky-order-and-time.txt create mode 100644 junit-mockito/output/q30-repeated-test-failure-threshold.txt create mode 100644 junit-mockito/pom.xml create mode 100755 junit-mockito/scripts/compare-5-vs-6.sh create mode 100755 junit-mockito/scripts/no-agent.sh create mode 100755 junit-mockito/scripts/run-all.sh create mode 100644 junit-mockito/src/main/java/lab/jm/Counter.java create mode 100644 junit-mockito/src/main/java/lab/jm/FinalPricer.java create mode 100644 junit-mockito/src/main/java/lab/jm/Ids.java create mode 100644 junit-mockito/src/main/java/lab/jm/OrderService.java create mode 100644 junit-mockito/src/main/java/lab/jm/PaymentGateway.java create mode 100644 junit-mockito/src/main/java/lab/jm/Receipt.java create mode 100644 junit-mockito/src/test/java/lab/jm/A_JUnitFundamentalsTest.java create mode 100644 junit-mockito/src/test/java/lab/jm/B_ExtensionsTest.java create mode 100644 junit-mockito/src/test/java/lab/jm/C_ParameterizedAndDynamicTest.java create mode 100644 junit-mockito/src/test/java/lab/jm/D_MockitoCoreTest.java create mode 100644 junit-mockito/src/test/java/lab/jm/E_MockitoAdvancedTest.java create mode 100644 junit-mockito/src/test/java/lab/jm/F_FlakyTest.java create mode 100644 junit-mockito/src/test/java/lab/jm/Lab.java create mode 100644 junit-mockito/src/test/java/lab/jm/Run.java create mode 100644 junit-mockito/src/test/java/lab/jm/fixtures/ExtensionFixtures.java create mode 100644 junit-mockito/src/test/java/lab/jm/fixtures/FlakyFixtures.java create mode 100644 junit-mockito/src/test/java/lab/jm/fixtures/LifecycleFixtures.java create mode 100644 junit-mockito/src/test/java/lab/jm/fixtures/Log.java create mode 100644 junit-mockito/src/test/java/lab/jm/fixtures/MockitoFixtures.java create mode 100644 junit-mockito/src/test/java/lab/jm/fixtures/ParamFixtures.java diff --git a/README.md b/README.md index fda5326..3ff8a58 100644 --- a/README.md +++ b/README.md @@ -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 | | [`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 | +| [`junit-mockito`](junit-mockito/) | Top 30 JUnit 6 and Mockito Interview Questions (2026) | this commit | ## `collections` module diff --git a/junit-mockito/README.md b/junit-mockito/README.md new file mode 100644 index 0000000..bba6e92 --- /dev/null +++ b/junit-mockito/README.md @@ -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. diff --git a/junit-mockito/output/q01-platform-jupiter-vintage.txt b/junit-mockito/output/q01-platform-jupiter-vintage.txt new file mode 100644 index 0000000..45bda0a --- /dev/null +++ b/junit-mockito/output/q01-platform-jupiter-vintage.txt @@ -0,0 +1,6 @@ +TestEngines found on the classpath (ServiceLoader): + 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 diff --git a/junit-mockito/output/q02-jar-comparison.txt b/junit-mockito/output/q02-jar-comparison.txt new file mode 100644 index 0000000..a3107e4 --- /dev/null +++ b/junit-mockito/output/q02-jar-comparison.txt @@ -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 diff --git a/junit-mockito/output/q02-junit6-baseline-and-removals.txt b/junit-mockito/output/q02-junit6-baseline-and-removals.txt new file mode 100644 index 0000000..b635349 --- /dev/null +++ b/junit-mockito/output/q02-junit6-baseline-and-removals.txt @@ -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) diff --git a/junit-mockito/output/q03-lifecycle-order.txt b/junit-mockito/output/q03-lifecycle-order.txt new file mode 100644 index 0000000..dd57a73 --- /dev/null +++ b/junit-mockito/output/q03-lifecycle-order.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) diff --git a/junit-mockito/output/q04-per-class-lifecycle.txt b/junit-mockito/output/q04-per-class-lifecycle.txt new file mode 100644 index 0000000..6192163 --- /dev/null +++ b/junit-mockito/output/q04-per-class-lifecycle.txt @@ -0,0 +1,3 @@ +@BeforeAll (non-static) on instance #1 +second(): instance #1, counter=1 +first(): instance #1, counter=2 diff --git a/junit-mockito/output/q05-nested-lifecycle.txt b/junit-mockito/output/q05-nested-lifecycle.txt new file mode 100644 index 0000000..d235c7c --- /dev/null +++ b/junit-mockito/output/q05-nested-lifecycle.txt @@ -0,0 +1,8 @@ +outer @BeforeEach + outerTest() +outer @AfterEach +outer @BeforeEach +inner @BeforeEach + innerTest() +inner @AfterEach +outer @AfterEach diff --git a/junit-mockito/output/q06-method-order.txt b/junit-mockito/output/q06-method-order.txt new file mode 100644 index 0000000..300eece --- /dev/null +++ b/junit-mockito/output/q06-method-order.txt @@ -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()] diff --git a/junit-mockito/output/q07-assert-all-and-assert-throws.txt b/junit-mockito/output/q07-assert-all-and-assert-throws.txt new file mode 100644 index 0000000..783f9ee --- /dev/null +++ b/junit-mockito/output/q07-assert-all-and-assert-throws.txt @@ -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: but was: +assertThrows returns the exception: getMessage() = boom diff --git a/junit-mockito/output/q08-timeout-vs-preemptive.txt b/junit-mockito/output/q08-timeout-vs-preemptive.txt new file mode 100644 index 0000000..46f0405 --- /dev/null +++ b/junit-mockito/output/q08-timeout-vs-preemptive.txt @@ -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 diff --git a/junit-mockito/output/q09-assertion-vs-assumption.txt b/junit-mockito/output/q09-assertion-vs-assumption.txt new file mode 100644 index 0000000..8bc80aa --- /dev/null +++ b/junit-mockito/output/q09-assertion-vs-assumption.txt @@ -0,0 +1,3 @@ +assertionFails() -> FAILED +assumptionFails() -> ABORTED +passes() -> SUCCESSFUL diff --git a/junit-mockito/output/q10-extension-callback-order.txt b/junit-mockito/output/q10-extension-callback-order.txt new file mode 100644 index 0000000..dd68f8b --- /dev/null +++ b/junit-mockito/output/q10-extension-callback-order.txt @@ -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 diff --git a/junit-mockito/output/q11-extend-with-vs-register-extension.txt b/junit-mockito/output/q11-extend-with-vs-register-extension.txt new file mode 100644 index 0000000..9562795 --- /dev/null +++ b/junit-mockito/output/q11-extend-with-vs-register-extension.txt @@ -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 diff --git a/junit-mockito/output/q12-parameter-resolver.txt b/junit-mockito/output/q12-parameter-resolver.txt new file mode 100644 index 0000000..ad28090 --- /dev/null +++ b/junit-mockito/output/q12-parameter-resolver.txt @@ -0,0 +1,2 @@ +custom parameter : Tenant[name=tenant-for-resolvesCustomAndBuiltIn] +built-in TestInfo: displayName=resolvesCustomAndBuiltIn(Tenant, TestInfo) diff --git a/junit-mockito/output/q13-parameterized-sources.txt b/junit-mockito/output/q13-parameterized-sources.txt new file mode 100644 index 0000000..236a664 --- /dev/null +++ b/junit-mockito/output/q13-parameterized-sources.txt @@ -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 diff --git a/junit-mockito/output/q14-csv-null-and-empty.txt b/junit-mockito/output/q14-csv-null-and-empty.txt new file mode 100644 index 0000000..044f2fb --- /dev/null +++ b/junit-mockito/output/q14-csv-null-and-empty.txt @@ -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" diff --git a/junit-mockito/output/q15-dynamic-tests.txt b/junit-mockito/output/q15-dynamic-tests.txt new file mode 100644 index 0000000..3c0a59a --- /dev/null +++ b/junit-mockito/output/q15-dynamic-tests.txt @@ -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 diff --git a/junit-mockito/output/q16-disabled-conditional-tags.txt b/junit-mockito/output/q16-disabled-conditional-tags.txt new file mode 100644 index 0000000..4c104ab --- /dev/null +++ b/junit-mockito/output/q16-disabled-conditional-tags.txt @@ -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 diff --git a/junit-mockito/output/q17-temp-dir.txt b/junit-mockito/output/q17-temp-dir.txt new file mode 100644 index 0000000..e5cf5fb --- /dev/null +++ b/junit-mockito/output/q17-temp-dir.txt @@ -0,0 +1,2 @@ +inside the test : dir exists = true, files = 1 +after the test finished : dir exists = false diff --git a/junit-mockito/output/q18-mock-vs-spy.txt b/junit-mockito/output/q18-mock-vs-spy.txt new file mode 100644 index 0000000..d494aa1 --- /dev/null +++ b/junit-mockito/output/q18-mock-vs-spy.txt @@ -0,0 +1,2 @@ +mock: next() -> 0, next() -> 0, real code ran 0 times +spy : next() -> 1, next() -> 2, real code ran 2 times diff --git a/junit-mockito/output/q19-when-vs-doreturn-on-spy.txt b/junit-mockito/output/q19-when-vs-doreturn-on-spy.txt new file mode 100644 index 0000000..270681d --- /dev/null +++ b/junit-mockito/output/q19-when-vs-doreturn-on-spy.txt @@ -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 diff --git a/junit-mockito/output/q20-verify-times-never-inorder.txt b/junit-mockito/output/q20-verify-times-never-inorder.txt new file mode 100644 index 0000000..cc66a3b --- /dev/null +++ b/junit-mockito/output/q20-verify-times-never-inorder.txt @@ -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: diff --git a/junit-mockito/output/q21-matchers-and-captor.txt b/junit-mockito/output/q21-matchers-and-captor.txt new file mode 100644 index 0000000..dc0ba5e --- /dev/null +++ b/junit-mockito/output/q21-matchers-and-captor.txt @@ -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 diff --git a/junit-mockito/output/q22-strict-stubs.txt b/junit-mockito/output/q22-strict-stubs.txt new file mode 100644 index 0000000..f883ea0 --- /dev/null +++ b/junit-mockito/output/q22-strict-stubs.txt @@ -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 diff --git a/junit-mockito/output/q23-stubbing-styles.txt b/junit-mockito/output/q23-stubbing-styles.txt new file mode 100644 index 0000000..d1e103e --- /dev/null +++ b/junit-mockito/output/q23-stubbing-styles.txt @@ -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 diff --git a/junit-mockito/output/q24-final-classes-mock-maker.txt b/junit-mockito/output/q24-final-classes-mock-maker.txt new file mode 100644 index 0000000..57f35f0 --- /dev/null +++ b/junit-mockito/output/q24-final-classes-mock-maker.txt @@ -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) diff --git a/junit-mockito/output/q25-mock-static.txt b/junit-mockito/output/q25-mock-static.txt new file mode 100644 index 0000000..6d8fb23 --- /dev/null +++ b/junit-mockito/output/q25-mock-static.txt @@ -0,0 +1,4 @@ +before : REAL-ID +inside the scope : MOCK-ID +other thread sees : REAL-ID +after the scope : REAL-ID diff --git a/junit-mockito/output/q26-mock-construction.txt b/junit-mockito/output/q26-mock-construction.txt new file mode 100644 index 0000000..11a9576 --- /dev/null +++ b/junit-mockito/output/q26-mock-construction.txt @@ -0,0 +1,4 @@ +before : RECEIPT[tea] +inside : FAKE [tea] +constructed during the scope: 1 +after : RECEIPT[tea] diff --git a/junit-mockito/output/q27-no-agent-warning.txt b/junit-mockito/output/q27-no-agent-warning.txt new file mode 100644 index 0000000..125245c --- /dev/null +++ b/junit-mockito/output/q27-no-agent-warning.txt @@ -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 diff --git a/junit-mockito/output/q28-unstubbed-defaults.txt b/junit-mockito/output/q28-unstubbed-defaults.txt new file mode 100644 index 0000000..ffdcc80 --- /dev/null +++ b/junit-mockito/output/q28-unstubbed-defaults.txt @@ -0,0 +1,6 @@ +int -> 0 +boolean -> false +String -> null +List -> [] +Optional -> Optional.empty +long -> 0 diff --git a/junit-mockito/output/q29-flaky-order-and-time.txt b/junit-mockito/output/q29-flaky-order-and-time.txt new file mode 100644 index 0000000..a1b6b2a --- /dev/null +++ b/junit-mockito/output/q29-flaky-order-and-time.txt @@ -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 diff --git a/junit-mockito/output/q30-repeated-test-failure-threshold.txt b/junit-mockito/output/q30-repeated-test-failure-threshold.txt new file mode 100644 index 0000000..242af95 --- /dev/null +++ b/junit-mockito/output/q30-repeated-test-failure-threshold.txt @@ -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 diff --git a/junit-mockito/pom.xml b/junit-mockito/pom.xml new file mode 100644 index 0000000..0a417c3 --- /dev/null +++ b/junit-mockito/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + com.ankurm.interview + junit-mockito + 1.0.0 + + + 25 + UTF-8 + 6.1.3 + 5.24.0 + + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar + + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + + + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.platform + junit-platform-launcher + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.6.0 + + ${mockito.agentArg} -Duser.timezone=UTC -Dfile.encoding=UTF-8 + + + + + diff --git a/junit-mockito/scripts/compare-5-vs-6.sh b/junit-mockito/scripts/compare-5-vs-6.sh new file mode 100755 index 0000000..8bd2d45 --- /dev/null +++ b/junit-mockito/scripts/compare-5-vs-6.sh @@ -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" diff --git a/junit-mockito/scripts/no-agent.sh b/junit-mockito/scripts/no-agent.sh new file mode 100755 index 0000000..c286997 --- /dev/null +++ b/junit-mockito/scripts/no-agent.sh @@ -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" diff --git a/junit-mockito/scripts/run-all.sh b/junit-mockito/scripts/run-all.sh new file mode 100755 index 0000000..3b03b33 --- /dev/null +++ b/junit-mockito/scripts/run-all.sh @@ -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/" diff --git a/junit-mockito/src/main/java/lab/jm/Counter.java b/junit-mockito/src/main/java/lab/jm/Counter.java new file mode 100644 index 0000000..dd58da1 --- /dev/null +++ b/junit-mockito/src/main/java/lab/jm/Counter.java @@ -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; + } +} diff --git a/junit-mockito/src/main/java/lab/jm/FinalPricer.java b/junit-mockito/src/main/java/lab/jm/FinalPricer.java new file mode 100644 index 0000000..2e4edea --- /dev/null +++ b/junit-mockito/src/main/java/lab/jm/FinalPricer.java @@ -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; + } +} diff --git a/junit-mockito/src/main/java/lab/jm/Ids.java b/junit-mockito/src/main/java/lab/jm/Ids.java new file mode 100644 index 0000000..e1a253c --- /dev/null +++ b/junit-mockito/src/main/java/lab/jm/Ids.java @@ -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"; + } +} diff --git a/junit-mockito/src/main/java/lab/jm/OrderService.java b/junit-mockito/src/main/java/lab/jm/OrderService.java new file mode 100644 index 0000000..7fc77ec --- /dev/null +++ b/junit-mockito/src/main/java/lab/jm/OrderService.java @@ -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)); + } +} diff --git a/junit-mockito/src/main/java/lab/jm/PaymentGateway.java b/junit-mockito/src/main/java/lab/jm/PaymentGateway.java new file mode 100644 index 0000000..d6555b4 --- /dev/null +++ b/junit-mockito/src/main/java/lab/jm/PaymentGateway.java @@ -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); +} diff --git a/junit-mockito/src/main/java/lab/jm/Receipt.java b/junit-mockito/src/main/java/lab/jm/Receipt.java new file mode 100644 index 0000000..70be0ca --- /dev/null +++ b/junit-mockito/src/main/java/lab/jm/Receipt.java @@ -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(); + } +} diff --git a/junit-mockito/src/test/java/lab/jm/A_JUnitFundamentalsTest.java b/junit-mockito/src/test/java/lab/jm/A_JUnitFundamentalsTest.java new file mode 100644 index 0000000..86faf0f --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/A_JUnitFundamentalsTest.java @@ -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 engines = new ArrayList<>(); + for (TestEngine e : ServiceLoader.load(TestEngine.class)) { + engines.add(e.getId() + " " + e.getVersion().orElse("?")); + } + p("TestEngines found on the classpath (ServiceLoader):"); + 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 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 byName = Map.of("junit.jupiter.testmethod.order.default", + org.junit.jupiter.api.MethodOrderer.MethodName.class.getName()); + Map 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")); + } +} diff --git a/junit-mockito/src/test/java/lab/jm/B_ExtensionsTest.java b/junit-mockito/src/test/java/lab/jm/B_ExtensionsTest.java new file mode 100644 index 0000000..5335d02 --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/B_ExtensionsTest.java @@ -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)); + } +} diff --git a/junit-mockito/src/test/java/lab/jm/C_ParameterizedAndDynamicTest.java b/junit-mockito/src/test/java/lab/jm/C_ParameterizedAndDynamicTest.java new file mode 100644 index 0000000..8d11abc --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/C_ParameterizedAndDynamicTest.java @@ -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)); + } +} diff --git a/junit-mockito/src/test/java/lab/jm/D_MockitoCoreTest.java b/junit-mockito/src/test/java/lab/jm/D_MockitoCoreTest.java new file mode 100644 index 0000000..62c69fa --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/D_MockitoCoreTest.java @@ -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 account = ArgumentCaptor.forClass(String.class); + ArgumentCaptor 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 -> %s", m.list()); + p("Optional -> %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 list(); + + Optional 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()); + } +} diff --git a/junit-mockito/src/test/java/lab/jm/E_MockitoAdvancedTest.java b/junit-mockito/src/test/java/lab/jm/E_MockitoAdvancedTest.java new file mode 100644 index 0000000..f34ebb3 --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/E_MockitoAdvancedTest.java @@ -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 otherThread = new AtomicReference<>(); + try (MockedStatic 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 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")); + } +} diff --git a/junit-mockito/src/test/java/lab/jm/F_FlakyTest.java b/junit-mockito/src/test/java/lab/jm/F_FlakyTest.java new file mode 100644 index 0000000..d24f24d --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/F_FlakyTest.java @@ -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); + } +} diff --git a/junit-mockito/src/test/java/lab/jm/Lab.java b/junit-mockito/src/test/java/lab/jm/Lab.java new file mode 100644 index 0000000..b64f37b --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/Lab.java @@ -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 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); + } + } +} diff --git a/junit-mockito/src/test/java/lab/jm/Run.java b/junit-mockito/src/test/java/lab/jm/Run.java new file mode 100644 index 0000000..9d3e022 --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/Run.java @@ -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 events = new ArrayList<>(); + final List 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 params) { + return of(fixture, params, null); + } + + static Run of(Class fixture, Map 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; + } +} diff --git a/junit-mockito/src/test/java/lab/jm/fixtures/ExtensionFixtures.java b/junit-mockito/src/test/java/lab/jm/fixtures/ExtensionFixtures.java new file mode 100644 index 0000000..b7a1b9e --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/fixtures/ExtensionFixtures.java @@ -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()); + } + } +} diff --git a/junit-mockito/src/test/java/lab/jm/fixtures/FlakyFixtures.java b/junit-mockito/src/test/java/lab/jm/fixtures/FlakyFixtures.java new file mode 100644 index 0000000..656b73f --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/fixtures/FlakyFixtures.java @@ -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 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"); + } + } +} diff --git a/junit-mockito/src/test/java/lab/jm/fixtures/LifecycleFixtures.java b/junit-mockito/src/test/java/lab/jm/fixtures/LifecycleFixtures.java new file mode 100644 index 0000000..6960f76 --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/fixtures/LifecycleFixtures.java @@ -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"); + } + } +} diff --git a/junit-mockito/src/test/java/lab/jm/fixtures/Log.java b/junit-mockito/src/test/java/lab/jm/fixtures/Log.java new file mode 100644 index 0000000..92f576e --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/fixtures/Log.java @@ -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 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 lines() { + return new ArrayList<>(LINES); + } +} diff --git a/junit-mockito/src/test/java/lab/jm/fixtures/MockitoFixtures.java b/junit-mockito/src/test/java/lab/jm/fixtures/MockitoFixtures.java new file mode 100644 index 0000000..1ba1226 --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/fixtures/MockitoFixtures.java @@ -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"); + } + } +} diff --git a/junit-mockito/src/test/java/lab/jm/fixtures/ParamFixtures.java b/junit-mockito/src/test/java/lab/jm/fixtures/ParamFixtures.java new file mode 100644 index 0000000..0c18e40 --- /dev/null +++ b/junit-mockito/src/test/java/lab/jm/fixtures/ParamFixtures.java @@ -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 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 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()); + } + } +}