Level 7 - Milestone 11

Milestone 11 - Unit Testing the Repository Class

In this section we will complete the unit testing of the repository-layer class

Before completion of this milestone, students will:

  • Add a class/unit tests for their repository class
  • Learn how to unit test code that involves WebClient

Create a Class for the Repository Test

This will be accomplished the exact same way you created a test class for the previous controller and service classes.

Unit Testing with WebClient

The one significant difference with unit testing our repository class is that we have to deal with mocking our WebClient instance directly. Because our WebClient code exists in this repository class, we will need to undergo a significant amount of mocking and stubbing to successfully avoid making an actual call to the external service from which we are receiving data. As with the WebClient code itself, students should not necessarily feel like they need to be able to recreate this code from memory. At this level of complexity, it would be perfectly understandable to find this code, either on the internet or in a previously-completed project, and reuse it for this class.

First thing we need to do is get access to a mocked WebClient object that we can use for our test. In our repository's constructor, you will notice that the WebClient is not autowired, but instead we wrote some code to build it within the method. We need a way to get a the mocked WebClient which we will create in our unit test into the instance of our repository which we will use during our testing. The easiest way to accomplish this is to simply add another constructor to our repository class that takes a WebClient as a parameter. Then we can use that constructor for our test class to pass-in the mock of our WebClient for use with the test.

Creating even more mocks

The WebClient code that makes the request to the external API in our repository class looks something like this:

return webClient.get()
    .uri(uriBuilder -> uriBuilder
        .queryParam("fo", "json")
        .queryParam("at", "results")
        .queryParam("q", query)
        .build()
    ).retrieve()
    .bodyToMono(LocResponse.class)
    .block()
    .getResults();
                

Unfortunately, each one of these methods that we have chained together returns a different type of object. That means that if we want to successful unit test this code, we will need to create a mock for each one of those intermediary objects. Including all of those mocks, the repository test class should look something like this:
class LocRepositoryTest {

    private LocRepository locRepository;

    @Mock
    WebClient webClientMock;

    @Mock
    WebClient.RequestHeadersUriSpec requestHeadersUriSpecMock;

    @Mock
    WebClient.RequestHeadersSpec requestHeadersSpecMock;

    @Mock
    WebClient.ResponseSpec responseSpecMock;

    @Mock
    Mono<LocResponse> LocResponseMonoMock;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);

        locRepository = new LocRepository(webClientMock);
    }

    @Test
    void whenGetResults_thenReturnLocResponse() {
        //given
        //when
        //then
    }

}
                    

Creating an LocResponse in our Test Class

Next, we will need to create an instance of LocResponse within our test class. You could do this in the setUp() method and save it as a field if you need to use it in multiple tests, but since we only have a single method to test, we can also place that within the "given" section of the test method itself. We can also save the query in a variable within our method, while we may not need to reuse it in this test, often times you will find that you will be required to pass that value as a parameter to multiple method calls.

@Test
void whenGetResults_thenReturnLocResponse() {
    String query = "Java";
    LocResponse locResponse = new LocResponse();
    Result result = new Result();
    result.setTitle("Java: A Drink, an Island, and a Programming Language");
    result.setAuthors(Collections.singletonList("AUTHOR"));
    result.setLink("LINK");
    List<Result> expectedResults = Collections.singletonList(result);
    locResponse.setResults(expectedResults);

    //when
    //then
}
                    

Stubbing the WebClient Methods

This is the challenging part. Again, it is perfectly acceptable to copy and paste this section of code if you have it available somewhere. We need to stub all of the methods that are chained together in our WebClient request, so that at the end of it we are left with our expected LocResponse, instead of getting a NullPointerException somewhere along the way.

@Test
void whenGetResults_thenReturnLocResponse() {
    //given
    String query = "Java";
    LocResponse locResponse = new LocResponse();
    Result result = new Result();
    result.setTitle("Java: A Drink, an Island, and a Programming Language");
    result.setAuthors(Collections.singletonList("AUTHOR"));
    result.setLink("LINK");
    List<Result> expectedResults = Collections.singletonList(result);
    locResponse.setResults(expectedResults);

    when(webClientMock.get())
            .thenReturn(requestHeadersUriSpecMock);
    when(requestHeadersUriSpecMock.uri((Function<UriBuilder, URI>) any()))
            .thenReturn(requestHeadersSpecMock);
    when(requestHeadersSpecMock.retrieve())
            .thenReturn(responseSpecMock);
    when(responseSpecMock.bodyToMono(LocResponse.class))
            .thenReturn(LocResponseMonoMock);
    when(LocResponseMonoMock.block())
            .thenReturn(locResponse);

    //when
    //then

}
                    

Finishing the Test

All that is left to do is call the repository method and make the assertion, much like we have in the previous tests. Completed, our unit test should look like this:

@Test
void whenGetResults_thenReturnLocResponse() {
    //given
    String query = "Java";
    LocResponse locResponse = new LocResponse();
    Result result = new Result();
    result.setTitle("Java: A Drink, an Island, and a Programming Language");
    result.setAuthors(Collections.singletonList("AUTHOR"));
    result.setLink("LINK");
    List<Result> expectedResults = Collections.singletonList(result);
    locResponse.setResults(expectedResults);

    when(webClientMock.get())
            .thenReturn(requestHeadersUriSpecMock);
    when(requestHeadersUriSpecMock.uri((Function<UriBuilder, URI>) any()))
            .thenReturn(requestHeadersSpecMock);
    when(requestHeadersSpecMock.retrieve())
            .thenReturn(responseSpecMock);
    when(responseSpecMock.bodyToMono(LocResponse.class))
            .thenReturn(LocResponseMonoMock);
    when(LocResponseMonoMock.block())
            .thenReturn(locResponse);

    //when
    List<Result> actualLocResults = locRepository.getResults(query);

    //then
    assertEquals(expectedResults, actualLocResults);
}
                    

You should now be able to run the test and see it pass.

Summary of Code Changes for this Milestone

    Cheetah-Search
    • src
      • main
        • java
          • org.jointheleague.level7.cheetah
            • config
              • ApiDocConfig.java
            • presentation
              • HomeController.java
              • LocController.java
            • service
              • LocService.java
            • repository
              • dto
                • Result.java
                • LocResponse.java
          • resources
            • application.yml
      • test
        • java
          • org.jointheleague.level7.cheetah
            • presentation
              • HomeControllerTest.java
              • LocControllerTest.java
            • service
              • LocServiceTest.java
            • repository
    • build.gradle