Add testing-no-containers module: WireMock, MockWebServer, @MockitoBean/@MockitoSpyBean (post #43)
This commit is contained in:
+28
@@ -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");
|
||||
}
|
||||
}
|
||||
+22
@@ -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");
|
||||
}
|
||||
}
|
||||
+29
@@ -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");
|
||||
}
|
||||
}
|
||||
+65
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user