From 5851521420156e3f7be65986d1cff3cd8356d1f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 06:09:47 +0000 Subject: [PATCH] Add testing-no-containers module: WireMock, MockWebServer, @MockitoBean/@MockitoSpyBean (post #43) --- README.md | 3 +- testing-no-containers/README.md | 56 ++++++++ .../01-mockbean-removed-compiler-error.txt | 10 ++ .../output/02-wiremock-bare-fatalstartup.txt | 26 ++++ .../03-wiremock-plus-jetty12-noclassdef.txt | 38 ++++++ .../docs/output/04-full-test-run-passing.txt | 129 ++++++++++++++++++ .../05-truncated-body-wrong-exception.txt | 36 +++++ testing-no-containers/pom.xml | 84 ++++++++++++ .../com/ankurm/testingnc/Application.java | 11 ++ .../com/ankurm/testingnc/GreetingService.java | 5 + .../ankurm/testingnc/GreetingServiceImpl.java | 11 ++ .../main/java/com/ankurm/testingnc/Quote.java | 4 + .../com/ankurm/testingnc/QuoteClient.java | 38 ++++++ .../src/main/resources/application.properties | 2 + .../testingnc/GreetingMockitoBeanATest.java | 28 ++++ .../testingnc/GreetingMockitoBeanBTest.java | 22 +++ .../GreetingMockitoSpyBeanCTest.java | 29 ++++ .../QuoteClientMockWebServerTest.java | 65 +++++++++ .../testingnc/QuoteClientWireMockTest.java | 68 +++++++++ .../src/test/resources/application.properties | 2 + 20 files changed, 666 insertions(+), 1 deletion(-) create mode 100644 testing-no-containers/README.md create mode 100644 testing-no-containers/docs/output/01-mockbean-removed-compiler-error.txt create mode 100644 testing-no-containers/docs/output/02-wiremock-bare-fatalstartup.txt create mode 100644 testing-no-containers/docs/output/03-wiremock-plus-jetty12-noclassdef.txt create mode 100644 testing-no-containers/docs/output/04-full-test-run-passing.txt create mode 100644 testing-no-containers/docs/output/05-truncated-body-wrong-exception.txt create mode 100644 testing-no-containers/pom.xml create mode 100644 testing-no-containers/src/main/java/com/ankurm/testingnc/Application.java create mode 100644 testing-no-containers/src/main/java/com/ankurm/testingnc/GreetingService.java create mode 100644 testing-no-containers/src/main/java/com/ankurm/testingnc/GreetingServiceImpl.java create mode 100644 testing-no-containers/src/main/java/com/ankurm/testingnc/Quote.java create mode 100644 testing-no-containers/src/main/java/com/ankurm/testingnc/QuoteClient.java create mode 100644 testing-no-containers/src/main/resources/application.properties create mode 100644 testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoBeanATest.java create mode 100644 testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoBeanBTest.java create mode 100644 testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoSpyBeanCTest.java create mode 100644 testing-no-containers/src/test/java/com/ankurm/testingnc/QuoteClientMockWebServerTest.java create mode 100644 testing-no-containers/src/test/java/com/ankurm/testingnc/QuoteClientWireMockTest.java create mode 100644 testing-no-containers/src/test/resources/application.properties diff --git a/README.md b/README.md index 9a79fce..350c1c6 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,9 @@ files. | [`i18n/`](i18n) | [Internationalization (i18n) in Spring Boot 4: MessageSource, LocaleResolver and Localized ProblemDetail](https://ankurm.com/spring-boot-4-internationalization-messagesource-localeresolver-problemdetail/) | English, Marathi and Hindi message bundles, `Accept-Language` vs a cookie resolver, apostrophes and Devanagari digits in `MessageFormat`, the JVM default locale as a hidden fallback, localized validation messages and `ProblemDetail` titles | | [`aot-cache/`](aot-cache) | [Faster Spring Boot Startup with the JDK AOT Cache (JEP 514/515): Benchmarks vs CDS and Native](https://ankurm.com/spring-boot-jdk-aot-cache-jep-514-515-startup-benchmarks/) | the JDK 25 AOT cache trained two ways, against a plain JVM, AppCDS, Spring's own AOT output and a GraalVM native image (ten interleaved rounds each), first-request latency, 24 cases where the cache silently stops applying (jar, timestamp, JVM build, ZGC, compact headers), `-XX:AOTMode=on`, Docker layer arithmetic | | [`kotlin/`](kotlin) | [Kotlin with Spring Boot 4.1: Null Safety, Coroutines, and What Java Developers Get Wrong](https://ankurm.com/) | Kotlin 2.3.21 (Boot 4.1.1's own managed version, not the latest GA), the real `@Configuration class may not be final` and `@Bean method must not be private or final` failures without the allopen/noarg `spring`/`jpa` compiler plugins, `suspend fun`/`Flow` controller methods proven to still resume off a Tomcat thread under plain `spring-boot-starter-webmvc`, a JSpecify `@NullMarked` Java method that still NPEs at runtime because the contract isn't runtime-checked, and `TestRestTemplate` confirmed absent from Boot 4.1's `spring-boot-test` jar | +| [`testing-no-containers/`](testing-no-containers) | [Testing Spring Boot 4 Without Containers: WireMock, MockWebServer and @MockitoBean](https://ankurm.com/) | `@MockBean`'s total removal proven with a real `javac` failure, `@MockitoBean`/`@MockitoSpyBean` context-cache behavior proven with real `org.springframework.test.context.cache` DEBUG log lines, WireMock fault injection against a real `RestClient`, a real `NoClassDefFoundError` from mixing `wiremock` with `wiremock-jetty12` (fixed with `wiremock-standalone`), and MockWebServer's `SocketEffect.ShutdownConnection` for genuinely severing a connection mid-response | -`core-di`, `core-beans`, `custom-starter`, `core-events`, `multi-datasource`, `i18n`, `aot-cache`, `kotlin` and `hexagonal` are the exception: they have **no `docs/` folder**. Their deeper material lives in +`core-di`, `core-beans`, `custom-starter`, `core-events`, `multi-datasource`, `i18n`, `aot-cache`, `kotlin`, `testing-no-containers` and `hexagonal` are the exception: they have **no `docs/` folder**. Their deeper material lives in collapsible sections inside the articles themselves, and their captured output sits in a top-level `output/` directory instead of `docs/output/` (`hexagonal` is also the one multi-module reactor in this repo, so its output sits one level deeper still, under each module: `core/output/`, diff --git a/testing-no-containers/README.md b/testing-no-containers/README.md new file mode 100644 index 0000000..029c410 --- /dev/null +++ b/testing-no-containers/README.md @@ -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. diff --git a/testing-no-containers/docs/output/01-mockbean-removed-compiler-error.txt b/testing-no-containers/docs/output/01-mockbean-removed-compiler-error.txt new file mode 100644 index 0000000..8873c9d --- /dev/null +++ b/testing-no-containers/docs/output/01-mockbean-removed-compiler-error.txt @@ -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 diff --git a/testing-no-containers/docs/output/02-wiremock-bare-fatalstartup.txt b/testing-no-containers/docs/output/02-wiremock-bare-fatalstartup.txt new file mode 100644 index 0000000..6690ed5 --- /dev/null +++ b/testing-no-containers/docs/output/02-wiremock-bare-fatalstartup.txt @@ -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.(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 diff --git a/testing-no-containers/docs/output/03-wiremock-plus-jetty12-noclassdef.txt b/testing-no-containers/docs/output/03-wiremock-plus-jetty12-noclassdef.txt new file mode 100644 index 0000000..7555f42 --- /dev/null +++ b/testing-no-containers/docs/output/03-wiremock-plus-jetty12-noclassdef.txt @@ -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.(JettyHttpServer.java:79) + at com.github.tomakehurst.wiremock.jetty12.Jetty12HttpServer.(Jetty12HttpServer.java:74) + at com.github.tomakehurst.wiremock.jetty12.Jetty12HttpServerFactory.buildHttpServer(Jetty12HttpServerFactory.java:33) + at com.github.tomakehurst.wiremock.WireMockServer.(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 diff --git a/testing-no-containers/docs/output/04-full-test-run-passing.txt b/testing-no-containers/docs/output/04-full-test-run-passing.txt new file mode 100644 index 0000000..d112c8f --- /dev/null +++ b/testing-no-containers/docs/output/04-full-test-run-passing.txt @@ -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] ------------------------------------------------------------------------ diff --git a/testing-no-containers/docs/output/05-truncated-body-wrong-exception.txt b/testing-no-containers/docs/output/05-truncated-body-wrong-exception.txt new file mode 100644 index 0000000..8f644bb --- /dev/null +++ b/testing-no-containers/docs/output/05-truncated-body-wrong-exception.txt @@ -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 diff --git a/testing-no-containers/pom.xml b/testing-no-containers/pom.xml new file mode 100644 index 0000000..fdc7ab9 --- /dev/null +++ b/testing-no-containers/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.1.1 + + + + com.ankurm + testing-no-containers + 1.0.0 + testing-no-containers + Testing Spring Boot 4 without containers: @MockitoBean/@MockitoSpyBean, WireMock fault injection, MockWebServer for RestClient, and context-cache behavior + jar + + + 25 + + 3.13.2 + 5.5.0 + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-restclient + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.wiremock + wiremock-standalone + ${wiremock.version} + test + + + + + com.squareup.okhttp3 + mockwebserver3-junit5 + ${mockwebserver.version} + test + + + + + testing-no-containers + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/testing-no-containers/src/main/java/com/ankurm/testingnc/Application.java b/testing-no-containers/src/main/java/com/ankurm/testingnc/Application.java new file mode 100644 index 0000000..3c55004 --- /dev/null +++ b/testing-no-containers/src/main/java/com/ankurm/testingnc/Application.java @@ -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); + } +} diff --git a/testing-no-containers/src/main/java/com/ankurm/testingnc/GreetingService.java b/testing-no-containers/src/main/java/com/ankurm/testingnc/GreetingService.java new file mode 100644 index 0000000..1117b87 --- /dev/null +++ b/testing-no-containers/src/main/java/com/ankurm/testingnc/GreetingService.java @@ -0,0 +1,5 @@ +package com.ankurm.testingnc; + +public interface GreetingService { + String greet(String name); +} diff --git a/testing-no-containers/src/main/java/com/ankurm/testingnc/GreetingServiceImpl.java b/testing-no-containers/src/main/java/com/ankurm/testingnc/GreetingServiceImpl.java new file mode 100644 index 0000000..e9a73fc --- /dev/null +++ b/testing-no-containers/src/main/java/com/ankurm/testingnc/GreetingServiceImpl.java @@ -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 + "!"; + } +} diff --git a/testing-no-containers/src/main/java/com/ankurm/testingnc/Quote.java b/testing-no-containers/src/main/java/com/ankurm/testingnc/Quote.java new file mode 100644 index 0000000..e4375b5 --- /dev/null +++ b/testing-no-containers/src/main/java/com/ankurm/testingnc/Quote.java @@ -0,0 +1,4 @@ +package com.ankurm.testingnc; + +public record Quote(long id, String text, String author) { +} diff --git a/testing-no-containers/src/main/java/com/ankurm/testingnc/QuoteClient.java b/testing-no-containers/src/main/java/com/ankurm/testingnc/QuoteClient.java new file mode 100644 index 0000000..53ef7f6 --- /dev/null +++ b/testing-no-containers/src/main/java/com/ankurm/testingnc/QuoteClient.java @@ -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); + } +} diff --git a/testing-no-containers/src/main/resources/application.properties b/testing-no-containers/src/main/resources/application.properties new file mode 100644 index 0000000..cc5e0df --- /dev/null +++ b/testing-no-containers/src/main/resources/application.properties @@ -0,0 +1,2 @@ +spring.application.name=testing-no-containers +quotes.api.base-url=http://localhost:9999 diff --git a/testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoBeanATest.java b/testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoBeanATest.java new file mode 100644 index 0000000..5bec192 --- /dev/null +++ b/testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoBeanATest.java @@ -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"); + } +} diff --git a/testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoBeanBTest.java b/testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoBeanBTest.java new file mode 100644 index 0000000..f6417b4 --- /dev/null +++ b/testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoBeanBTest.java @@ -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"); + } +} diff --git a/testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoSpyBeanCTest.java b/testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoSpyBeanCTest.java new file mode 100644 index 0000000..1e07bf2 --- /dev/null +++ b/testing-no-containers/src/test/java/com/ankurm/testingnc/GreetingMockitoSpyBeanCTest.java @@ -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"); + } +} diff --git a/testing-no-containers/src/test/java/com/ankurm/testingnc/QuoteClientMockWebServerTest.java b/testing-no-containers/src/test/java/com/ankurm/testingnc/QuoteClientMockWebServerTest.java new file mode 100644 index 0000000..d4afac0 --- /dev/null +++ b/testing-no-containers/src/test/java/com/ankurm/testingnc/QuoteClientMockWebServerTest.java @@ -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); + } +} diff --git a/testing-no-containers/src/test/java/com/ankurm/testingnc/QuoteClientWireMockTest.java b/testing-no-containers/src/test/java/com/ankurm/testingnc/QuoteClientWireMockTest.java new file mode 100644 index 0000000..f701301 --- /dev/null +++ b/testing-no-containers/src/test/java/com/ankurm/testingnc/QuoteClientWireMockTest.java @@ -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); + } +} diff --git a/testing-no-containers/src/test/resources/application.properties b/testing-no-containers/src/test/resources/application.properties new file mode 100644 index 0000000..bb301cb --- /dev/null +++ b/testing-no-containers/src/test/resources/application.properties @@ -0,0 +1,2 @@ +quotes.api.base-url=http://localhost:9999 +logging.level.org.springframework.test.context.cache=DEBUG