Improve your test structure with Lambdas and Mockito’s Answer

No Comments

Although the use of mock objects is controversial, we as developers have to use them from time to time. The nearly 6000 stars Mockito has on GitHub indicate that others would agree with this statement. Especially when we are dealing with library classes that we cannot easily instantiate or with classes that establish some connection like HTTP, mocks show their strength. In order to make tests more readable, Java’s lambdas and Mockito’s Answer can help us.

Motivating example

One class that is a good candidate for mocking is Spring’s RestTemplate. In order to have an easy to set up and fast test we usually do not want to ramp-up the complete Spring Application Context. We would rather mock the RestTemplate and return some pre-canned responses. To give you an example I created a simple service that retrieves Chuck Norris facts. You can find the example on GitHub.

A simple approach to mocking the RestTemplate often results in test code that looks like this:

public class ChuckNorrisServiceNeedsRefactoringTest {
 
    private static final Long EXISTING_JOKE = 1L;
    private static final Map<String, Long> GOOD_HTTP_PARAMS = Collections.singletonMap("id", EXISTING_JOKE);
    private static final Long NON_EXISTING_JOKE = 15123123L;
    private static final Map<String, Long> NON_EXISTING_HTTP_PARAMS = Collections.singletonMap("id", NON_EXISTING_JOKE);
    private static final Long BAD_JOKE = 99999999L;
    private static final Map<String, Long> BAD_HTTP_PARAMS = Collections.singletonMap("id", BAD_JOKE);
 
    private static final ResponseEntity<ChuckNorrisFactResponse> ERROR_RESPONSE =
            new ResponseEntity<>(new ChuckNorrisFactResponse("NoSuchQuoteException", "No quote with id=15123123."), HttpStatus.OK);
    private static final ResponseEntity<ChuckNorrisFactResponse> ITEM_RESPONSE =
            new ResponseEntity<>(new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, "Chuck Norris is awesome")), HttpStatus.OK);
 
    @Test
    public void serviceShouldReturnFact() {
        RestTemplate restTemplate = mock(RestTemplate.class);
        when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, GOOD_HTTP_PARAMS))
                .thenReturn(ITEM_RESPONSE);
        ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
 
        ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(EXISTING_JOKE);
 
        assertThat(chuckNorrisFact, is(new ChuckNorrisFact(EXISTING_JOKE, "Chuck Norris is awesome")));
    }
 
    @Test
    public void serviceShouldReturnNothing() {
        RestTemplate restTemplate = mock(RestTemplate.class);
        when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, NON_EXISTING_HTTP_PARAMS))
                .thenReturn(ERROR_RESPONSE);
        ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
 
        ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(NON_EXISTING_JOKE);
 
        assertThat(chuckNorrisFact, is(nullValue()));
    }
 
    @Test(expected = ResourceAccessException.class)
    public void serviceShouldPropagateException() {
        RestTemplate restTemplate = mock(RestTemplate.class);
        when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, BAD_HTTP_PARAMS))
                .thenThrow(new ResourceAccessException("I/O error"));
        ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
 
        myServiceUnderTest.retrieveFact(BAD_JOKE);
    }
}

In this test, the two Mockito methods mock() and when() are statically imported. mock() creates the RestTemplate mock object and when() records the behaviour that is expected.

This test code is not too bad, but also not too good. We already see some repetition (we should keep our code DRY) and if we would ever switch from the RestTemplate to something else we will have to touch every test. Therefore, let’s see how we can improve this.

We can clearly see that extracting a method could improve the first two tests. This method then takes the answer and the http parameter and configures the mock. The third test method does not fit the schema because it throws an exception instead of returning a ResponseEntity. Next to the duplication, we are actually dealing too much with technical details here. When reading the tests, do we really need to know if GET or POST is being executed? Do we even have to know the type of the response? What we actually care about is how the ChuckNorrisService behaves. The HTTP communication is hidden inside it.

Lambdas to the rescue

This is where Lambdas can help us to improve our test structure. Next to the probably well known Mockito methods thenReturn and thenThrow there is also thenAnswer. This method expects a parameter implementing the generic Answer interface, which can do basically anything. The advantage is that an Answer can compute the value it returns. This differs from the values which thenReturn and thenThrow take because those are fixed. I do not know if it was intentional or not, but Mockito’s Answer interface fulfills the requirements of a Java 8 functional interface. With its single method T answer(InvocationOnMock invocation) throws Throwable; it is equivalent to java.util.function.Function. The only difference is the throws. Having this knowledge, we can get rid of the code duplication and show clearly what our intention in the test is.

To start, I will directly show you the refactored version of the example above:

public class ChuckNorrisServiceStepOneTest {
 
    private static final Long EXISTING_JOKE = 1L;
    private static final Map<String, Long> GOOD_HTTP_PARAMS = Collections.singletonMap("id", EXISTING_JOKE);
    private static final Long NON_EXISTING_JOKE = 15123123L;
    private static final Map<String, Long> NON_EXISTING_HTTP_PARAMS = Collections.singletonMap("id", NON_EXISTING_JOKE);
    private static final Long BAD_JOKE = 99999999L;
    private static final Map<String, Long> BAD_HTTP_PARAMS = Collections.singletonMap("id", BAD_JOKE);
 
    private static final ResponseEntity<ChuckNorrisFactResponse> ERROR_RESPONSE =
            new ResponseEntity<>(new ChuckNorrisFactResponse("NoSuchQuoteException", "No quote with id=15123123."), HttpStatus.OK);
    private static final ResponseEntity<ChuckNorrisFactResponse> ITEM_RESPONSE =
            new ResponseEntity<>(new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, "Chuck Norris is awesome")), HttpStatus.OK);
 
    @Test
    public void serviceShouldReturnFact() {
        RestTemplate restTemplate = restEndpointShouldAnswer(GOOD_HTTP_PARAMS, (invocation) -> ITEM_RESPONSE);
        ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
 
        ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(EXISTING_JOKE);
 
        assertThat(chuckNorrisFact, is(new ChuckNorrisFact(EXISTING_JOKE, "Chuck Norris is awesome")));
    }
 
    @Test
    public void serviceShouldReturnNothing() {
        RestTemplate restTemplate = restEndpointShouldAnswer(NON_EXISTING_HTTP_PARAMS, (invocation -> ERROR_RESPONSE));
        ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
 
        ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(NON_EXISTING_JOKE);
 
        assertThat(chuckNorrisFact, is(nullValue()));
    }
 
    @Test(expected = ResourceAccessException.class)
    public void serviceShouldPropagateException() {
        RestTemplate restTemplate = restEndpointShouldAnswer(BAD_HTTP_PARAMS, (invocation -> {throw new ResourceAccessException("I/O error");}));
        ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
 
        myServiceUnderTest.retrieveFact(BAD_JOKE);
    }
 
    private RestTemplate restEndpointShouldAnswer(Map<String, Long> httpParams, Answer<ResponseEntity<ChuckNorrisFactResponse>> response){
        RestTemplate restTemplate = mock(RestTemplate.class);
        when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, httpParams)).thenAnswer(response);
        return restTemplate;
    }
}

So, what did improve? Firstly, we can directly see how the HTTP parameter correspond to certain responses. We do not have to skim through the test to match parameters and reponses. Secondly, when reading a single test the details of the REST invocation are now hidden from us. We do not need to know about the URL, HTTP method and response class unless we really have to. Lastly, we managed to unify the handling of the RestTemplate mock by extracting a method. The “normal” answers and the exception are no longer treated differently. Changing the REST call from GET to POST would only require to change one line in the test.

Further refactoring

What we did not solve is spreading the RestTemplate all over the place. By using fields and @Before we can trim down the test even more;

public class ChuckNorrisServiceStepTwoTest {
 
    private static final Long EXISTING_JOKE = 1L;
    private static final Map<String, Long> GOOD_HTTP_PARAMS = Collections.singletonMap("id", EXISTING_JOKE);
    private static final Long NON_EXISTING_JOKE = 15123123L;
    private static final Map<String, Long> NON_EXISTING_HTTP_PARAMS = Collections.singletonMap("id", NON_EXISTING_JOKE);
    private static final Long BAD_JOKE = 99999999L;
    private static final Map<String, Long> BAD_HTTP_PARAMS = Collections.singletonMap("id", BAD_JOKE);
 
    private static final ResponseEntity<ChuckNorrisFactResponse> ERROR_RESPONSE =
            new ResponseEntity<>(new ChuckNorrisFactResponse("NoSuchQuoteException", "No quote with id=15123123."), HttpStatus.OK);
    private static final ResponseEntity<ChuckNorrisFactResponse> ITEM_RESPONSE =
            new ResponseEntity<>(new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, "Chuck Norris is awesome")), HttpStatus.OK);
 
    private RestTemplate restTemplate;
    private ChuckNorrisService myServiceUnderTest;
 
    @Before
    public void setUp(){
        restTemplate = mock(RestTemplate.class);
        myServiceUnderTest = new ChuckNorrisService(restTemplate);
    }
 
    @Test
    public void serviceShouldReturnFact() {
        restEndpointShouldAnswer(GOOD_HTTP_PARAMS, (invocation) -> ITEM_RESPONSE);
 
        ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(EXISTING_JOKE);
 
        assertThat(chuckNorrisFact, is(new ChuckNorrisFact(EXISTING_JOKE, "Chuck Norris is awesome")));
    }
 
    @Test
    public void serviceShouldReturnNothing() {
        restEndpointShouldAnswer(NON_EXISTING_HTTP_PARAMS, (invocation -> ERROR_RESPONSE));
 
        ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(NON_EXISTING_JOKE);
 
        assertThat(chuckNorrisFact, is(nullValue()));
    }
 
    @Test(expected = ResourceAccessException.class)
    public void serviceShouldPropagateException() {
        restEndpointShouldAnswer(BAD_HTTP_PARAMS, (invocation -> {throw new ResourceAccessException("I/O error");}));
 
        myServiceUnderTest.retrieveFact(BAD_JOKE);
    }
 
    private void restEndpointShouldAnswer(Map<String, Long> httpParams, Answer<ResponseEntity<ChuckNorrisFactResponse>> response){
        when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, httpParams)).thenAnswer(response);
    }
}

Using fields and moving the instantiation of the class under test into the test setup might not be advantageous in every case but we cannot deny that it removes even more repetition. Also, the restEndpointShouldAnswer() method looks cleaner without a return value.

Conclusion

An important point we should keep in mind when writing tests is to make clear what their intention is, i.e. what we actually want to test. If we cannot clearly see what the test actual does and asserts, it will be hard to change the test in the future. Additionally, it can be hard to check whether the class under test is thoroughly tested. Using Lambdas to refactor mocking and to extract duplicated code helps us to improve the test structure as well as the readability.

Ronny Bräunlich

Ronny works since May 2017 for the codecentric AG. He is convinced about TDD and works mostly in the JVM ecosystem.

Comment

Your email address will not be published. Required fields are marked *