Yet another blog post about a Mockito feature that we should rarely use: Mockito deep stubs. With this article, we'll explore how deep stubbing can reduce the boilerplate stub setup for our tests when chaining fluent APIs of a mocked class. In general, “Every time a mock returns a mock, a fairy dies” should be our guiding principle when working with Mockito. We almost always end up with an exact copy of our implementation when using this feature. This makes our tests brittle and our code harder to refactor.
Please note that I'm not advocating making excessive use of this feature. However, it still worth knowing what the tool we're using (Mockito) provides.
The Starting Point
Fluent APIs are great. Chaining methods of a fluent API make our code more concise as we don't store intermediate results inside a variable. We deal with such fluent APIs when writing code that involves e.g., the Stream
API, a builder pattern, HTTP clients, etc.
However, as soon as we're mocking the class for which we chain multiple methods, stubbing the mock is less trivial and, most of the time, counterproductive.
Let's take a look at an example.
We're going to test an HTTP client class that uses the Spring WebFlux WebClient to fetch a random quote from a remote system:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class InspirationalQuotesClient{ // HTTP client from Spring WebFlux private final WebClient webClient; public InspirationalQuotesClient(WebClient webClient) { this.webClient = webClient; } public String fetchRandomQuote() { try { return this.webClient .get() .uri("/api/quotes") .retrieve() .bodyToMono(String.class) .block(); } catch (WebClientException webClientException) { return "Every time a mock returns, a mock a fairy dies."; } } } |
When we now want to write a unit test for this class, and as long as we're not aware of this Mockito feature and don't know a better alternative (i.e., what the Java Testing Ecosystem offers), our test class becomes a mocking hell.
The Problem With Normal Stubbing
Let's assume we've recently started our Java testing journey and are familiar with JUnit and Mockito. We now want to write a unit test for the InspirationalQuotesClient
and mock any collaborator of our class under test. In this example, that's the WebClient
.
Unfortunately, it's not just the usual Mockito one-liner for this test setup to provide the stubbing setup. Many methods of our mock are invoked, and they're even chained. After some research, several trips to Stack Overflow, and some head crashing, we finally have something working.
This brings us to the following test setup for verifying our InspirationalQuotesClient
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @ExtendWith(MockitoExtension.class) class InspirationalQuotesClientTest { @Mock private WebClient webClient; @InjectMocks private InspirationalQuotesClient cut; // class under test @Test void shouldReturnInMockingHell() { WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mockito.mock(WebClient.RequestHeadersUriSpec.class); WebClient.ResponseSpec responseSpec = Mockito.mock(WebClient.ResponseSpec.class); when(webClient.get()).thenReturn(requestHeadersUriSpec); when(requestHeadersUriSpec.uri("/api/quotes")).thenReturn(requestHeadersUriSpec); when(requestHeadersUriSpec.retrieve()).thenReturn(responseSpec); when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just("We've escaped hell")); String result = cut.fetchRandomQuote(); assertEquals("We've escaped hell", result); } } |
As there is no computational complexity (no if/else no loops, etc.) inside our implementation, the unit test can only verify that whatever .retrieve()
returns, our client is converting to a non-reactive type. We could have gone further and also mock the Mono
but would violate one of the central rules of Mockito to not mock value/data objects.
Consider how this test and especially the stubbing setup grows when we chain further method calls of the WebClient
. We have to provide a stubbing for each particular part inside this chain and exactly match (or use generic ArgumentMatchers
) the usage of the methods. That's quite some work.
Furthermore, we also need some expertise in the class/framework we're using and understand its internals. With the test above, we tightly couple the verification to the internals of the implementation and end up with a brittle test which won't help whenever we refactor our client as each change in the method chain will fail our test.
That's definitely something we don't from our tests. Our tests should back up our refactoring efforts and provide stability.
A Possible Solution: Mockito Deep Stubs
As an alternative, we can use Mockito's deep stubs and refactor our test.
However, this technical and advanced feature doesn't magically make our test better. It just reduces the setup noise:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @ExtendWith(MockitoExtension.class) class DeepStubClientTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private WebClient webClient; @InjectMocks private InspirationalQuotesClient cut; // class under test @Test void shouldReturnQuoteFromRemoteSystem() { Mockito .when(webClient .get() .uri("/api/quotes") .retrieve() .bodyToMono(String.class)) .thenReturn(Mono.just("Less setup hell - but not better")); String result = cut.fetchRandomQuote(); assertEquals("Less setup hell - but not better", result); } } |
The important part here is the additional attribute for the @Mock
annotation:
1 2 | @Mock(answer = Answers.RETURNS_DEEP_STUBS) private WebClient webClient; |
In case we're not using this annotation, we can also instrument Mockito to create a mock that returns deep stubs with the following approach:
1 | WebClient webClient = Mockito.mock(WebClient.class, Answers.RETURNS_DEEP_STUBS); |
The upside of this setup is the reduced boilerplate code to stub the method chaining. We Mockito's deep stubs, we chain the invocation of our mock as it's used inside our class under test (another indicator we're literally copying our implementation).
We can also use ArgumentMatchers
to provide a more generic stubbing setup:
1 2 3 4 5 6 7 | Mockito .when(webClient .get() .uri(ArgumentMatchers.anyString()) // match any URI .retrieve() .bodyToMono(String.class)) .thenReturn(Mono.just("Less setup hell - but not better")); |
Apart from the obvious downside that this is an (almost) exact copy of our actual class under test, the deep stubs have some further limitations.
First, we can only verify the last mock in the chain and nothing in between:
1 2 3 4 5 6 7 | // Works Mockito.verify(webClient.get().uri("/quotes").retrieve()).bodyToMono(String.class); // The following verifications won't work and fail Mockito.verify(webClient.get().uri("/quotes")).retrieve(); Mockito.verify(webClient.get()).uri("/quotes"); Mockito.verify(webClient).get(); |
Next, the deep stubbing won't work whenever one method returns in this method chain return a non-mockable type. That's the case for primitive types (e.g. int
, double
, char
) or final classes (as long as we're not using the InlineMockMaker
).
With those two limitations and the fact that such tests don't support our refactoring efforts, we should rarely use this feature and try to find a better solution for our test.
A Better Solution: MockWebServer
Instead of mocking everything and using deep stubs with Mockito, we can do better.
Another (and better) alternative to testing this particular class is to write a proper HTTP client test. Using the MockWebServer from OkHttp3, we can spin up a local HTTP server in a matter of seconds and stub HTTP responses to test your InspirationalQuotesClient
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class DontHurtFairyTest { private MockWebServer mockWebServer; private InspirationalQuotesClient cut; @BeforeEach public void setup() throws IOException { this.mockWebServer = new MockWebServer(); this.mockWebServer.start(); this.cut = new InspirationalQuotesClient(WebClient .builder() .baseUrl(mockWebServer.url("/").toString()) .build()); } @Test void shouldReturnDefaultQuoteOnRemoteSystemFailure() throws InterruptedException { MockResponse mockResponse = new MockResponse() .setResponseCode(500); mockWebServer.enqueue(mockResponse); String result = cut.fetchRandomQuote(); assertEquals("Every time a mock returns, a mock a fairy dies.", result); RecordedRequest request = mockWebServer.takeRequest(); assertEquals("/api/quotes", request.getPath()); } } |
This testing recipe works for any other Java HTTP client.
As a summary and outcome of this article: Keep in mind that Mockito provides a deep stubbing feature. Use it with caution and only if there's no better alternative available.
For more practical Mockito advice, consider enrolling in the Hands-On Mocking With Mockito Online Course to learn the ins and outs of the most popular mocking framework for Java applications.
The source code for this Mockito deep stubbing example is available on GitHub.
PS: Make sure to hurt the least amount of fairies with your tests.
Joyful testing,
Philip