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:
1public class ChuckNorrisServiceNeedsRefactoringTest {
2
3 private static final Long EXISTING_JOKE = 1L;
4 private static final Map<String, Long> GOOD_HTTP_PARAMS = Collections.singletonMap("id", EXISTING_JOKE);
5 private static final Long NON_EXISTING_JOKE = 15123123L;
6 private static final Map<String, Long> NON_EXISTING_HTTP_PARAMS = Collections.singletonMap("id", NON_EXISTING_JOKE);
7 private static final Long BAD_JOKE = 99999999L;
8 private static final Map<String, Long> BAD_HTTP_PARAMS = Collections.singletonMap("id", BAD_JOKE);
9
10 private static final ResponseEntity<ChuckNorrisFactResponse> ERROR_RESPONSE =
11 new ResponseEntity<>(new ChuckNorrisFactResponse("NoSuchQuoteException", "No quote with id=15123123."), HttpStatus.OK);
12 private static final ResponseEntity<ChuckNorrisFactResponse> ITEM_RESPONSE =
13 new ResponseEntity<>(new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, "Chuck Norris is awesome")), HttpStatus.OK);
14
15 @Test
16 public void serviceShouldReturnFact() {
17 RestTemplate restTemplate = mock(RestTemplate.class);
18 when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, GOOD_HTTP_PARAMS))
19 .thenReturn(ITEM_RESPONSE);
20 ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
21
22 ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(EXISTING_JOKE);
23
24 assertThat(chuckNorrisFact, is(new ChuckNorrisFact(EXISTING_JOKE, "Chuck Norris is awesome")));
25 }
26
27 @Test
28 public void serviceShouldReturnNothing() {
29 RestTemplate restTemplate = mock(RestTemplate.class);
30 when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, NON_EXISTING_HTTP_PARAMS))
31 .thenReturn(ERROR_RESPONSE);
32 ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
33
34 ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(NON_EXISTING_JOKE);
35
36 assertThat(chuckNorrisFact, is(nullValue()));
37 }
38
39 @Test(expected = ResourceAccessException.class)
40 public void serviceShouldPropagateException() {
41 RestTemplate restTemplate = mock(RestTemplate.class);
42 when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, BAD_HTTP_PARAMS))
43 .thenThrow(new ResourceAccessException("I/O error"));
44 ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
45
46 myServiceUnderTest.retrieveFact(BAD_JOKE);
47 }
48}
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:
1public class ChuckNorrisServiceStepOneTest {
2
3 private static final Long EXISTING_JOKE = 1L;
4 private static final Map<String, Long> GOOD_HTTP_PARAMS = Collections.singletonMap("id", EXISTING_JOKE);
5 private static final Long NON_EXISTING_JOKE = 15123123L;
6 private static final Map<String, Long> NON_EXISTING_HTTP_PARAMS = Collections.singletonMap("id", NON_EXISTING_JOKE);
7 private static final Long BAD_JOKE = 99999999L;
8 private static final Map<String, Long> BAD_HTTP_PARAMS = Collections.singletonMap("id", BAD_JOKE);
9
10 private static final ResponseEntity<ChuckNorrisFactResponse> ERROR_RESPONSE =
11 new ResponseEntity<>(new ChuckNorrisFactResponse("NoSuchQuoteException", "No quote with id=15123123."), HttpStatus.OK);
12 private static final ResponseEntity<ChuckNorrisFactResponse> ITEM_RESPONSE =
13 new ResponseEntity<>(new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, "Chuck Norris is awesome")), HttpStatus.OK);
14
15 @Test
16 public void serviceShouldReturnFact() {
17 RestTemplate restTemplate = restEndpointShouldAnswer(GOOD_HTTP_PARAMS, (invocation) -> ITEM_RESPONSE);
18 ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
19
20 ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(EXISTING_JOKE);
21
22 assertThat(chuckNorrisFact, is(new ChuckNorrisFact(EXISTING_JOKE, "Chuck Norris is awesome")));
23 }
24
25 @Test
26 public void serviceShouldReturnNothing() {
27 RestTemplate restTemplate = restEndpointShouldAnswer(NON_EXISTING_HTTP_PARAMS, (invocation -> ERROR_RESPONSE));
28 ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
29
30 ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(NON_EXISTING_JOKE);
31
32 assertThat(chuckNorrisFact, is(nullValue()));
33 }
34
35 @Test(expected = ResourceAccessException.class)
36 public void serviceShouldPropagateException() {
37 RestTemplate restTemplate = restEndpointShouldAnswer(BAD_HTTP_PARAMS, (invocation -> {throw new ResourceAccessException("I/O error");}));
38 ChuckNorrisService myServiceUnderTest = new ChuckNorrisService(restTemplate);
39
40 myServiceUnderTest.retrieveFact(BAD_JOKE);
41 }
42
43 private RestTemplate restEndpointShouldAnswer(Map<String, Long> httpParams, Answer<ResponseEntity<ChuckNorrisFactResponse>> response){
44 RestTemplate restTemplate = mock(RestTemplate.class);
45 when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, httpParams)).thenAnswer(response);
46 return restTemplate;
47 }
48}
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;
1public class ChuckNorrisServiceStepTwoTest {
2
3 private static final Long EXISTING_JOKE = 1L;
4 private static final Map<String, Long> GOOD_HTTP_PARAMS = Collections.singletonMap("id", EXISTING_JOKE);
5 private static final Long NON_EXISTING_JOKE = 15123123L;
6 private static final Map<String, Long> NON_EXISTING_HTTP_PARAMS = Collections.singletonMap("id", NON_EXISTING_JOKE);
7 private static final Long BAD_JOKE = 99999999L;
8 private static final Map<String, Long> BAD_HTTP_PARAMS = Collections.singletonMap("id", BAD_JOKE);
9
10 private static final ResponseEntity<ChuckNorrisFactResponse> ERROR_RESPONSE =
11 new ResponseEntity<>(new ChuckNorrisFactResponse("NoSuchQuoteException", "No quote with id=15123123."), HttpStatus.OK);
12 private static final ResponseEntity<ChuckNorrisFactResponse> ITEM_RESPONSE =
13 new ResponseEntity<>(new ChuckNorrisFactResponse("success", new ChuckNorrisFact(1L, "Chuck Norris is awesome")), HttpStatus.OK);
14
15 private RestTemplate restTemplate;
16 private ChuckNorrisService myServiceUnderTest;
17
18 @Before
19 public void setUp(){
20 restTemplate = mock(RestTemplate.class);
21 myServiceUnderTest = new ChuckNorrisService(restTemplate);
22 }
23
24 @Test
25 public void serviceShouldReturnFact() {
26 restEndpointShouldAnswer(GOOD_HTTP_PARAMS, (invocation) -> ITEM_RESPONSE);
27
28 ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(EXISTING_JOKE);
29
30 assertThat(chuckNorrisFact, is(new ChuckNorrisFact(EXISTING_JOKE, "Chuck Norris is awesome")));
31 }
32
33 @Test
34 public void serviceShouldReturnNothing() {
35 restEndpointShouldAnswer(NON_EXISTING_HTTP_PARAMS, (invocation -> ERROR_RESPONSE));
36
37 ChuckNorrisFact chuckNorrisFact = myServiceUnderTest.retrieveFact(NON_EXISTING_JOKE);
38
39 assertThat(chuckNorrisFact, is(nullValue()));
40 }
41
42 @Test(expected = ResourceAccessException.class)
43 public void serviceShouldPropagateException() {
44 restEndpointShouldAnswer(BAD_HTTP_PARAMS, (invocation -> {throw new ResourceAccessException("I/O error");}));
45
46 myServiceUnderTest.retrieveFact(BAD_JOKE);
47 }
48
49 private void restEndpointShouldAnswer(Map<String, Long> httpParams, Answer<ResponseEntity<ChuckNorrisFactResponse>> response){
50 when(restTemplate.getForEntity(FACT_URL, ChuckNorrisFactResponse.class, httpParams)).thenAnswer(response);
51 }
52}
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.
More articles
fromRonny Bräunlich
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Ronny Bräunlich
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.