Add testing-no-containers module: WireMock, MockWebServer, @MockitoBean/@MockitoSpyBean (post #43)

This commit is contained in:
Claude
2026-10-08 06:19:52 +00:00
parent f736108067
commit 5851521420
20 changed files with 666 additions and 1 deletions
+56
View File
@@ -0,0 +1,56 @@
# testing-no-containers
Companion project for the article **[Testing Spring Boot 4 Without Containers: WireMock, MockWebServer and @MockitoBean](https://ankurm.com/)** on **[ankurm.com](https://ankurm.com)**.
Every console block quoted in the article came out of `docs/output/`. These are real `mvn test`
and `javac` transcripts, not retyped output — including two real, captured failures (a
`NoClassDefFoundError` from a bad WireMock dependency combination, and a `@MockBean` compiler
error) that are as much the point of the post as the passing tests are.
There is deliberately **no `docs/NN-topic.md` chapter folder**: the deeper material lives in
collapsible "going deeper" sections inside the article itself, next to the paragraph each one
extends. `docs/output/` (captured transcripts) is the one thing that still lives under `docs/`.
## Versions
| | |
|---|---|
| Spring Boot | 4.1.1 |
| Spring Framework | 7.0.9 |
| WireMock | 3.13.2 (`wiremock-standalone`; the 4.0.0 line is beta-only as of this writing — see the article) |
| OkHttp MockWebServer | 5.5.0 (`mockwebserver3-junit5`) |
| Mockito | 5.23.0 (Spring Boot 4.1.1's own managed version, via `mockito-bom`) |
| JDK | 25 (GraalVM CE 25.4.4.1.1) |
| Maven | 3.9 |
## Quickstart
```bash
export JAVA_HOME=/path/to/jdk-25
mvn test # runs everything below; no Docker, no Testcontainers, no running server required
```
## What's demonstrated
| Area | Where |
|---|---|
| `@MockBean` is completely removed in Spring Boot 4 (not deprecated — the package is gone) | `docs/output/01-mockbean-removed-compiler-error.txt` |
| `@MockitoBean`/`@MockitoSpyBean` (Spring Framework 7's replacement) mocking a collaborator with no running server | `GreetingMockitoBeanATest.java`, `GreetingMockitoBeanBTest.java` |
| The identical `@MockitoBean` override set is the `ApplicationContext` cache key: two classes with the same override reuse one context | `GreetingMockitoBeanATest.java` / `GreetingMockitoBeanBTest.java`, proven via real `org.springframework.test.context.cache` DEBUG log lines |
| `@MockitoSpyBean` is a different override set than `@MockitoBean`, even against the same type — a genuine cache miss | `GreetingMockitoSpyBeanCTest.java` |
| WireMock fault injection (`Fault.CONNECTION_RESET_BY_PEER`, `withFixedDelay`) against a real `RestClient` | `QuoteClientWireMockTest.java` |
| The wrong way and the right way to pull a WireMock server into the classpath in the 3.x line | `docs/output/02-wiremock-bare-fatalstartup.txt`, `docs/output/03-wiremock-plus-jetty12-noclassdef.txt`, `pom.xml` |
| MockWebServer's lower-level socket primitives (`SocketEffect.ShutdownConnection`) for genuinely severing a connection mid-response, vs. just sending truncated JSON | `QuoteClientMockWebServerTest.java` |
| Boot 4.1's `ClientHttpRequestFactoryBuilder`/`HttpClientSettings` API (not the Boot-3-era `ClientHttpRequestFactorySettings`/`ClientHttpRequestFactories` pair, which no longer exists) | `QuoteClient.java` |
## Captured output
| File | What it shows |
|---|---|
| `01-mockbean-removed-compiler-error.txt` | a standalone `javac` run against code importing `org.springframework.boot.test.mock.mockito.MockBean`: "package ... does not exist" |
| `02-wiremock-bare-fatalstartup.txt` | the bare `wiremock` artifact alone: `FatalStartupException`, no `HttpServerFactory` on the classpath |
| `03-wiremock-plus-jetty12-noclassdef.txt` | `wiremock` + `wiremock-jetty12` together: a mismatched-Jetty-version `NoClassDefFoundError` at server startup |
| `04-full-test-run-passing.txt` | the real, full `mvn clean test` run: all 8 tests passing, including the real `org.springframework.test.context.cache` DEBUG lines that prove the hit/miss behavior |
Nothing here needs Docker, a running server, or a network connection outside the JVM — that is the
whole premise of the post.
@@ -0,0 +1,10 @@
Picked up JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=38339 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
/tmp/mockbean-removed-demo/com/ankurm/testingnc/LegacyMockBeanTest.java:5: error: package org.springframework.boot.test.mock.mockito does not exist
import org.springframework.boot.test.mock.mockito.MockBean;
^
/tmp/mockbean-removed-demo/com/ankurm/testingnc/LegacyMockBeanTest.java:10: error: cannot find symbol
@MockBean
^
symbol: class MockBean
location: class LegacyMockBeanTest
2 errors
@@ -0,0 +1,26 @@
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.924 s <<< FAILURE! -- in com.ankurm.testingnc.QuoteClientWireMockTest
[ERROR] com.ankurm.testingnc.QuoteClientWireMockTest -- Time elapsed: 0.924 s <<< ERROR!
com.github.tomakehurst.wiremock.common.FatalStartupException: Jetty 11 is not present and no suitable HttpServerFactory extension was found. Please ensure that the classpath includes a WireMock extension that provides an HttpServerFactory implementation. See http://wiremock.org/docs/extending-wiremock/ for more information.
at com.github.tomakehurst.wiremock.http.HttpServerFactoryLoader.couldNotFindSuitableServerException(HttpServerFactoryLoader.java:85)
at com.github.tomakehurst.wiremock.http.HttpServerFactoryLoader.pickMostAppropriateFrom(HttpServerFactoryLoader.java:73)
at com.github.tomakehurst.wiremock.http.HttpServerFactoryLoader.load(HttpServerFactoryLoader.java:57)
at com.github.tomakehurst.wiremock.WireMockServer.getHttpServerFactory(WireMockServer.java:89)
at com.github.tomakehurst.wiremock.WireMockServer.<init>(WireMockServer.java:72)
at com.github.tomakehurst.wiremock.junit5.WireMockExtension.startServerIfRequired(WireMockExtension.java:233)
at com.github.tomakehurst.wiremock.junit5.WireMockExtension.beforeAll(WireMockExtension.java:300)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
[ERROR] Errors:
[ERROR] QuoteClientWireMockTest ? FatalStartup Jetty 11 is not present and no suitable HttpServerFactory extension was found. Please ensure that the classpath includes a WireMock extension that provides an HttpServerFactory implementation. See http://wiremock.org/docs/extending-wiremock/ for more information.
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.5.6:test (default-test) on project testing-no-containers:
[ERROR]
[ERROR] See /home/claude/spring-boot-demo/testing-no-containers/target/surefire-reports for the individual test results.
[ERROR] See dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
@@ -0,0 +1,38 @@
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 1.463 s <<< FAILURE! -- in com.ankurm.testingnc.QuoteClientWireMockTest
[ERROR] com.ankurm.testingnc.QuoteClientWireMockTest -- Time elapsed: 1.463 s <<< ERROR!
java.lang.NoClassDefFoundError: org/eclipse/jetty/io/WriteFlusher$Listener
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:962)
at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:144)
at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:776)
at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:691)
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:620)
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:578)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
at com.github.tomakehurst.wiremock.jetty12.Jetty12HttpServer.createHttpConnector(Jetty12HttpServer.java:88)
at com.github.tomakehurst.wiremock.jetty.JettyHttpServer.<init>(JettyHttpServer.java:79)
at com.github.tomakehurst.wiremock.jetty12.Jetty12HttpServer.<init>(Jetty12HttpServer.java:74)
at com.github.tomakehurst.wiremock.jetty12.Jetty12HttpServerFactory.buildHttpServer(Jetty12HttpServerFactory.java:33)
at com.github.tomakehurst.wiremock.WireMockServer.<init>(WireMockServer.java:75)
at com.github.tomakehurst.wiremock.junit5.WireMockExtension.startServerIfRequired(WireMockExtension.java:233)
at com.github.tomakehurst.wiremock.junit5.WireMockExtension.beforeAll(WireMockExtension.java:300)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1604)
Caused by: java.lang.ClassNotFoundException: org.eclipse.jetty.io.WriteFlusher$Listener
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
... 16 more
[ERROR] Errors:
[ERROR] QuoteClientWireMockTest ? NoClassDefFound org/eclipse/jetty/io/WriteFlusher$Listener
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.5.6:test (default-test) on project testing-no-containers:
[ERROR]
[ERROR] See /home/claude/spring-boot-demo/testing-no-containers/target/surefire-reports for the individual test results.
[ERROR] See dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
@@ -0,0 +1,129 @@
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< com.ankurm:testing-no-containers >------------------
[INFO] Building testing-no-containers 1.0.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- clean:3.5.0:clean (default-clean) @ testing-no-containers ---
[INFO] Deleting /home/claude/spring-boot-demo/testing-no-containers/target
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ testing-no-containers ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ testing-no-containers ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 5 source files with javac [debug parameters release 25] to target/classes
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ testing-no-containers ---
[INFO] Copying 1 resource from src/test/resources to target/test-classes
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ testing-no-containers ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 5 source files with javac [debug parameters release 25] to target/test-classes
[INFO]
[INFO] --- surefire:3.5.6:test (default-test) @ testing-no-containers ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.ankurm.testingnc.GreetingMockitoSpyBeanCTest
11:46:56.481 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.ankurm.testingnc.GreetingMockitoSpyBeanCTest]: GreetingMockitoSpyBeanCTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
11:46:56.753 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.testingnc.Application for test class com.ankurm.testingnc.GreetingMockitoSpyBeanCTest
11:46:56.879 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.ankurm.testingnc.GreetingMockitoSpyBeanCTest]: GreetingMockitoSpyBeanCTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
11:46:56.886 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.testingnc.Application for test class com.ankurm.testingnc.GreetingMockitoSpyBeanCTest
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.1.1)
2026-10-08T11:46:57.415+05:30 INFO 13524 --- [ main] c.a.t.GreetingMockitoSpyBeanCTest : Starting GreetingMockitoSpyBeanCTest using Java 25.0.4.1.1 with PID 13524 (started by root in /home/claude/spring-boot-demo/testing-no-containers)
2026-10-08T11:46:57.418+05:30 INFO 13524 --- [ main] c.a.t.GreetingMockitoSpyBeanCTest : No active profile set, falling back to 1 default profile: "default"
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
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2026-10-08T11:47:01.581+05:30 INFO 13524 --- [ main] c.a.t.GreetingMockitoSpyBeanCTest : Started GreetingMockitoSpyBeanCTest in 4.598 seconds (process running for 6.85)
2026-10-08T11:47:01.589+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 0, parentContextCount = 0, hitCount = 0, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.607+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 1, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.611+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 2, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.613+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 3, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.637+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 4, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.638+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 5, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.647+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 6, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.766+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 7, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.768+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 8, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.773+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 9, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.791+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 10, missCount = 1, failureCount = 0]
2026-10-08T11:47:01.792+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 1, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 11, missCount = 1, failureCount = 0]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.679 s -- in com.ankurm.testingnc.GreetingMockitoSpyBeanCTest
[INFO] Running com.ankurm.testingnc.GreetingMockitoBeanBTest
2026-10-08T11:47:01.820+05:30 INFO 13524 --- [ main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.testingnc.GreetingMockitoBeanBTest]: GreetingMockitoBeanBTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2026-10-08T11:47:01.828+05:30 INFO 13524 --- [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.testingnc.Application for test class com.ankurm.testingnc.GreetingMockitoBeanBTest
2026-10-08T11:47:01.830+05:30 INFO 13524 --- [ main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.testingnc.GreetingMockitoBeanBTest]: GreetingMockitoBeanBTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2026-10-08T11:47:01.834+05:30 INFO 13524 --- [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.testingnc.Application for test class com.ankurm.testingnc.GreetingMockitoBeanBTest
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.1.1)
2026-10-08T11:47:01.986+05:30 INFO 13524 --- [ main] c.a.testingnc.GreetingMockitoBeanBTest : Starting GreetingMockitoBeanBTest using Java 25.0.4.1.1 with PID 13524 (started by root in /home/claude/spring-boot-demo/testing-no-containers)
2026-10-08T11:47:01.987+05:30 INFO 13524 --- [ main] c.a.testingnc.GreetingMockitoBeanBTest : No active profile set, falling back to 1 default profile: "default"
2026-10-08T11:47:02.978+05:30 INFO 13524 --- [ main] c.a.testingnc.GreetingMockitoBeanBTest : Started GreetingMockitoBeanBTest in 1.118 seconds (process running for 8.246)
2026-10-08T11:47:02.979+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 0, parentContextCount = 0, hitCount = 11, missCount = 2, failureCount = 0]
2026-10-08T11:47:02.981+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 12, missCount = 2, failureCount = 0]
2026-10-08T11:47:02.981+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 13, missCount = 2, failureCount = 0]
2026-10-08T11:47:02.982+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 14, missCount = 2, failureCount = 0]
2026-10-08T11:47:02.986+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 15, missCount = 2, failureCount = 0]
2026-10-08T11:47:02.988+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 16, missCount = 2, failureCount = 0]
2026-10-08T11:47:02.991+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 17, missCount = 2, failureCount = 0]
2026-10-08T11:47:02.999+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 18, missCount = 2, failureCount = 0]
2026-10-08T11:47:03.000+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 19, missCount = 2, failureCount = 0]
2026-10-08T11:47:03.003+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 20, missCount = 2, failureCount = 0]
2026-10-08T11:47:03.008+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 21, missCount = 2, failureCount = 0]
2026-10-08T11:47:03.009+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 22, missCount = 2, failureCount = 0]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.195 s -- in com.ankurm.testingnc.GreetingMockitoBeanBTest
[INFO] Running com.ankurm.testingnc.QuoteClientMockWebServerTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.729 s -- in com.ankurm.testingnc.QuoteClientMockWebServerTest
[INFO] Running com.ankurm.testingnc.QuoteClientWireMockTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.896 s -- in com.ankurm.testingnc.QuoteClientWireMockTest
[INFO] Running com.ankurm.testingnc.GreetingMockitoBeanATest
2026-10-08T11:47:05.788+05:30 INFO 13524 --- [ main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.testingnc.GreetingMockitoBeanATest]: GreetingMockitoBeanATest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2026-10-08T11:47:05.797+05:30 INFO 13524 --- [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.testingnc.Application for test class com.ankurm.testingnc.GreetingMockitoBeanATest
2026-10-08T11:47:05.808+05:30 INFO 13524 --- [ main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.testingnc.GreetingMockitoBeanATest]: GreetingMockitoBeanATest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2026-10-08T11:47:05.810+05:30 INFO 13524 --- [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.testingnc.Application for test class com.ankurm.testingnc.GreetingMockitoBeanATest
2026-10-08T11:47:05.813+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 0, parentContextCount = 0, hitCount = 23, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.815+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 24, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.816+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 25, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.817+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 26, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.819+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 27, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.822+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 28, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.822+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 29, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.824+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 30, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.826+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 31, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.827+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 32, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.828+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 33, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.829+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 34, missCount = 2, failureCount = 0]
2026-10-08T11:47:05.831+05:30 DEBUG 13524 --- [ main] org.springframework.test.context.cache : Spring test ApplicationContext cache statistics: [DefaultContextCache@778c2e7c size = 2, maxSize = 32, contextUsageCount = 1, parentContextCount = 0, hitCount = 35, missCount = 2, failureCount = 0]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.050 s -- in com.ankurm.testingnc.GreetingMockitoBeanATest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 16.113 s
[INFO] Finished at: 2026-10-08T11:47:06+05:30
[INFO] ------------------------------------------------------------------------
@@ -0,0 +1,36 @@
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.831 s <<< FAILURE! -- in com.ankurm.testingnc.QuoteClientMockWebServerTest
[ERROR] com.ankurm.testingnc.QuoteClientMockWebServerTest.truncatedBodyIsNotTheSameAsASeveredConnection -- Time elapsed: 1.564 s <<< FAILURE!
java.lang.AssertionError:
Expecting actual throwable to be an instance of:
org.springframework.web.client.ResourceAccessException
but was:
org.springframework.web.client.RestClientException: Error while extracting response for type [com.ankurm.testingnc.Quote] and content type [application/json]
at org.springframework.web.client.DefaultRestClient.readWithMessageConverters(DefaultRestClient.java:274)
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.readBody(DefaultRestClient.java:931)
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.lambda$body$0(DefaultRestClient.java:833)
...(93 remaining lines not displayed - this can be changed with Assertions.setMaxStackTraceElementsDisplayed)
at com.ankurm.testingnc.QuoteClientMockWebServerTest.truncatedBodyIsNotTheSameAsASeveredConnection(QuoteClientMockWebServerTest.java:55)
[ERROR] Failures:
[ERROR] QuoteClientMockWebServerTest.truncatedBodyIsNotTheSameAsASeveredConnection:55
Expecting actual throwable to be an instance of:
org.springframework.web.client.ResourceAccessException
but was:
org.springframework.web.client.RestClientException: Error while extracting response for type [com.ankurm.testingnc.Quote] and content type [application/json]
at org.springframework.web.client.DefaultRestClient.readWithMessageConverters(DefaultRestClient.java:274)
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.readBody(DefaultRestClient.java:931)
at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.lambda$body$0(DefaultRestClient.java:833)
...(93 remaining lines not displayed - this can be changed with Assertions.setMaxStackTraceElementsDisplayed)
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.5.6:test (default-test) on project testing-no-containers: There are test failures.
[ERROR]
[ERROR] See /home/claude/spring-boot-demo/testing-no-containers/target/surefire-reports for the individual test results.
[ERROR] See dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
+84
View File
@@ -0,0 +1,84 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
<artifactId>testing-no-containers</artifactId>
<version>1.0.0</version>
<name>testing-no-containers</name>
<description>Testing Spring Boot 4 without containers: @MockitoBean/@MockitoSpyBean, WireMock fault injection, MockWebServer for RestClient, and context-cache behavior</description>
<packaging>jar</packaging>
<properties>
<java.version>25</java.version>
<!-- org.wiremock:wiremock's 4.0.0 line is beta-only as of this writing (4.0.0-beta.1
through 4.0.0-beta.40 on Maven Central): 3.13.2 is the real latest GA; see the post. -->
<wiremock.version>3.13.2</wiremock.version>
<mockwebserver.version>5.5.0</mockwebserver.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- WireMock: standalone HTTP server with fault injection, not managed by
spring-boot-dependencies; pin it explicitly. The bare "wiremock" artifact no longer
bundles a server in the 3.x line (FatalStartupException: "Jetty 11 is not present and
no suitable HttpServerFactory extension was found"), and adding wiremock-jetty12 on top
of it is worse, not better: wiremock's own transitive deps still drag in real Jetty 11
modules (jetty-servlet, jetty-servlets, jetty-webapp, http2-server at 11.0.26) alongside
wiremock-jetty12's Jetty 12 ones, Maven's dependency mediation then picks one jetty-io
version for everyone, and the mismatched pair throws NoClassDefFoundError at server
startup for a class as basic as org.eclipse.jetty.io.WriteFlusher$Listener. The fix is
wiremock-standalone: a single shaded jar with its own relocated copy of Jetty
(wiremock.org.eclipse.jetty.*), so nothing on your own classpath can clash with it;
see the post. -->
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
<!-- MockWebServer: OkHttp's own stub server, JUnit 5 extension flavor. Also not
managed by spring-boot-dependencies. -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver3-junit5</artifactId>
<version>${mockwebserver.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>testing-no-containers</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,11 @@
package com.ankurm.testingnc;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@@ -0,0 +1,5 @@
package com.ankurm.testingnc;
public interface GreetingService {
String greet(String name);
}
@@ -0,0 +1,11 @@
package com.ankurm.testingnc;
import org.springframework.stereotype.Service;
@Service
public class GreetingServiceImpl implements GreetingService {
@Override
public String greet(String name) {
return "Hello, " + name + "!";
}
}
@@ -0,0 +1,4 @@
package com.ankurm.testingnc;
public record Quote(long id, String text, String author) {
}
@@ -0,0 +1,38 @@
package com.ankurm.testingnc;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.HttpClientSettings;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.time.Duration;
/**
* A deliberately simple HTTP client, built with the timeout wired so a test can actually
* trigger it -- see the fault-injection section of the post for why a 300ms read timeout
* matters. Boot 4.1 replaced the old {@code ClientHttpRequestFactorySettings}/
* {@code ClientHttpRequestFactories} pair (Boot 3.x) with {@link ClientHttpRequestFactoryBuilder}
* and the {@link HttpClientSettings} record -- a tutorial written against Boot 3 will not
* compile here; see the post.
*/
@Component
public class QuoteClient {
private final RestClient restClient;
public QuoteClient(@Value("${quotes.api.base-url}") String baseUrl) {
var settings = HttpClientSettings.defaults().withReadTimeout(Duration.ofMillis(300));
this.restClient = RestClient.builder()
.baseUrl(baseUrl)
.requestFactory(ClientHttpRequestFactoryBuilder.detect().build(settings))
.build();
}
public Quote getQuote(long id) {
return restClient.get()
.uri("/quotes/{id}", id)
.retrieve()
.body(Quote.class);
}
}
@@ -0,0 +1,2 @@
spring.application.name=testing-no-containers
quotes.api.base-url=http://localhost:9999
@@ -0,0 +1,28 @@
package com.ankurm.testingnc;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
/**
* Deliberately has the exact same @MockitoBean override as {@link GreetingMockitoBeanBTest}:
* same type, same (default) name, same reset mode. That identical override set is the context
* cache key, and the point of this pair is to prove, with a real captured log line, that the
* second class reuses the first class's ApplicationContext instead of building a new one.
*/
@SpringBootTest
class GreetingMockitoBeanATest {
@MockitoBean
GreetingService greetingService;
@Test
void mockedGreetingIsReturned() {
when(greetingService.greet("Ada")).thenReturn("yo Ada");
assertThat(greetingService.greet("Ada")).isEqualTo("yo Ada");
}
}
@@ -0,0 +1,22 @@
package com.ankurm.testingnc;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@SpringBootTest
class GreetingMockitoBeanBTest {
@MockitoBean
GreetingService greetingService;
@Test
void mockedGreetingIsReturnedHere_too() {
when(greetingService.greet("Linus")).thenReturn("yo Linus");
assertThat(greetingService.greet("Linus")).isEqualTo("yo Linus");
}
}
@@ -0,0 +1,29 @@
package com.ankurm.testingnc;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
/**
* A @MockitoSpyBean override is a different override set than @MockitoBean (A and B above),
* even though both target GreetingService -- a spy wraps the real bean, a mock replaces it
* entirely. Different override set, different cache key: this class should cost a brand new
* ApplicationContext, a real, captured cache miss.
*/
@SpringBootTest
class GreetingMockitoSpyBeanCTest {
@MockitoSpyBean
GreetingServiceImpl greetingService;
@Test
void realMethodRunsAndCanStillBeVerified() {
String result = greetingService.greet("Grace");
assertThat(result).isEqualTo("Hello, Grace!");
verify(greetingService).greet("Grace");
}
}
@@ -0,0 +1,65 @@
package com.ankurm.testingnc;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.RecordedRequest;
import mockwebserver3.SocketEffect;
import mockwebserver3.junit5.StartStop;
import org.junit.jupiter.api.Test;
import org.springframework.web.client.ResourceAccessException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Same QuoteClient, same scenarios, a different stub server. MockWebServer is a real OkHttp
* TCP server too -- not a mock of the HTTP client -- but it is lower-level than WireMock: no
* named "fault" API, so a reset connection or a slow response is built from the raw socket
* primitives OkHttp's MockResponse exposes.
*/
class QuoteClientMockWebServerTest {
@StartStop
MockWebServer server = new MockWebServer();
@Test
void returnsTheStubbedQuote() throws InterruptedException {
server.enqueue(new MockResponse.Builder()
.addHeader("Content-Type", "application/json")
.body("{\"id\":42,\"text\":\"Talk is cheap. Show me the code.\",\"author\":\"Linus Torvalds\"}")
.build());
var client = new QuoteClient(server.url("/").toString());
Quote quote = client.getQuote(42);
assertThat(quote.text()).isEqualTo("Talk is cheap. Show me the code.");
RecordedRequest recorded = server.takeRequest(1, TimeUnit.SECONDS);
assertThat(recorded).isNotNull();
assertThat(recorded.getTarget()).isEqualTo("/quotes/42");
}
@Test
void socketClosedMidResponseSurfacesAsResourceAccessException() {
// No named "connection reset" fault here, unlike WireMock -- it's built from the raw
// primitive: MockResponse.Builder.onResponseStart(SocketEffect) runs the effect right as
// the server begins writing the response. SocketEffect.ShutdownConnection tears down the
// TCP connection at that point, so the client never receives a complete (or even
// well-formed) response -- this is a genuine severed connection, not merely truncated
// JSON that still parses as a body. (An earlier draft of this test just sent a short,
// invalid JSON body and got a RestClientException -- a real response the client could
// read and fail to parse, which is a different failure mode than the connection actually
// dying. See the post.)
server.enqueue(new MockResponse.Builder()
.addHeader("Content-Type", "application/json")
.onResponseStart(SocketEffect.ShutdownConnection.INSTANCE)
.build());
var client = new QuoteClient(server.url("/").toString());
assertThatThrownBy(() -> client.getQuote(99))
.isInstanceOf(ResourceAccessException.class);
}
}
@@ -0,0 +1,68 @@
package com.ankurm.testingnc;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.web.client.ResourceAccessException;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* No Spring context here at all -- QuoteClient only needs a base URL string, so this is a
* plain JUnit 5 test against a real WireMock server. "Without containers" in this post means
* no Docker, not no Spring; this test also happens to need no Spring.
*/
class QuoteClientWireMockTest {
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance().build();
@Test
void returnsTheStubbedQuote() {
wireMock.stubFor(get(urlEqualTo("/quotes/42")).willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":42,\"text\":\"Talk is cheap. Show me the code.\",\"author\":\"Linus Torvalds\"}")));
var client = new QuoteClient(wireMock.baseUrl());
Quote quote = client.getQuote(42);
assertThat(quote.text()).isEqualTo("Talk is cheap. Show me the code.");
assertThat(quote.author()).isEqualTo("Linus Torvalds");
}
@Test
void connectionResetByPeerSurfacesAsResourceAccessException() {
wireMock.stubFor(get(urlEqualTo("/quotes/99")).willReturn(aResponse()
.withFault(com.github.tomakehurst.wiremock.http.Fault.CONNECTION_RESET_BY_PEER)));
var client = new QuoteClient(wireMock.baseUrl());
assertThatThrownBy(() -> client.getQuote(99))
.isInstanceOf(ResourceAccessException.class);
}
@Test
void fixedDelayPastTheReadTimeoutSurfacesAsResourceAccessException() {
// QuoteClient is built with a 300ms read timeout; this stub takes 1000ms to respond.
// ClientHttpRequestFactoryBuilder.detect() picks whichever HTTP client implementation
// is actually on the classpath, and that choice changes the wording: the JDK HttpClient
// reports a cancelled request as "Request cancelled" (java.net.http.HttpTimeoutException),
// while Apache HttpClient 5 would say "Read timed out" (java.net.SocketTimeoutException).
// Both are ResourceAccessException at the Spring level -- that type, not the message
// text, is the thing worth asserting on; see the post.
wireMock.stubFor(get(urlEqualTo("/quotes/7")).willReturn(aResponse()
.withFixedDelay(1000)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":7,\"text\":\"late\",\"author\":\"nobody\"}")));
var client = new QuoteClient(wireMock.baseUrl());
assertThatThrownBy(() -> client.getQuote(7))
.isInstanceOf(ResourceAccessException.class);
}
}
@@ -0,0 +1,2 @@
quotes.api.base-url=http://localhost:9999
logging.level.org.springframework.test.context.cache=DEBUG