94

Creating Deep Stubs With Mockito to Chain Method Stubbing

 3 years ago
source link: https://rieckpil.de/creating-deep-stubs-with-mockito-to-chain-method-stubbing/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Creating Deep Stubs With Mockito to Chain Method Stubbing

April 28, 2021

Get an overview of all topics you'll find answers for on this blog here. Also, make sure to grab your cheat sheet PDF copy JUnit 5 & Mockito - Spring MVC - Java EE.

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:

public class InspirationalQuotesClient{
  // HTTP client from Spring WebFlux
  private final WebClient webClient;
  public InspirationalQuotesClient(WebClient webClient) {
    this.webClient = webClient;
  public String fetchRandomQuote() {
      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.

Mockito & JUnit Cheat Sheet

JUnit 5 & Mockito Cheat Sheet

Answering 24 questions for the two most essential Java testing libraries.

This brings us to the following test setup for verifying our InspirationalQuotesClient:

@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:

@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:

@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:

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:

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:

// 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 :

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

Learn the Painless and Efficient Way to Test Any Java Application

  • Testing strategies & best practices
  • Testing recipes (database access, HTTP clients, web layer, etc.)
  • Independent of your application framework (Spring Boot, Jakarta EE, Quarkus, Micronaut, etc.)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK