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

This commit is contained in:
Claude
2026-10-08 06:09:47 +00:00
parent f736108067
commit 71820cb36d
19 changed files with 630 additions and 1 deletions
@@ -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);
}
}