Level 7 - Milestone 14

Milestone 14 - Integration Testing the LocController

In this section we will add an integration test for our LocController method.

Before completion of this milestone, students will:

  • Learn how to engage in mocking within an integration test
  • Learn how to write an integration test that examines the JSON object that is returned to the user

Creating a Class for the Integration Test

Add a LocControllerIntTest class inside the appropriate folder within the IntTest source set, keep in mind that the structure reflects the structure of our main and test source sets.

Add the @WebMvcTest annotation above the class declaration like we did in the HomeControllerIntTest. We can again specify the slice of the application that needs to be brought up for this test by updating the annotation to @WebMvcTest(LocController.class).

Again like the HomeControllerIntTest, we need to add an Autowired field that will be of type MockMvc. This object will allow us to execute the "fake" requests to the application.

Mocking Within Integration Tests

The main difference between our LocControllerIntTest class and our HomeControllerIntTest classes is very similar to the difference between our LocControllerTest and HomeControllerTest classes: we need to engage in mocking. If you do not recall the reason for mocking, you can refer back to Milestone 10. In this milestone, we will focus on the difference between how mocking takes place in unit tests and integration tests. In our LocControllerTest class, we created an instance of LocController upon which we directly called the methods that we wanted to test

@Mock
private LocService locService;

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

    locController = new LocController(locService);
}

Notice the first line, where we marked the LocService as a mock using the @Mock annotation, and then the final line of code where we passed that mock into the instance of LocController upon which we later directly called the method we want to test:
List<Result> actualResults = locController.getResults(query);

This worked great, and allowed us to ensure that any LocService methods called as a side effect of invoking methods LocController methods would be called on our mocked LocService.

We could try using this same strategy for supply mocks for our integration tests to use, but we would immediately run into an issue: we cannot simply pass in a mocked LocService when we instantiate an LocController, because we are never creating an instance of LocController! Similar to our extant HomeControllerIntTest, we will allow Spring to create all instances of the objects that the code within our main source set requires, just as it does when run normally. So then how to do we get a mock in there? It turns out that it is even more simple than how we inserted the mock for our unit tests, but the difficult part is understanding the need for doing it this way. In order to mock the LocService within our integration test, we only need to create a field to contain an LocService, and then annotate it with @MockBean (notice this is different from the previously-used @Mock annotation). Since we cannot pass a mock LocService into our instance of LocController as we did in our unit tests, this @MockBean annotation tells Spring to create a mock LocService, and simply use that in any scenario where an instance of LocService may be required!
@MockBean
private LocService locService;

Testing the Happy Path

Just like our LocControllerTest class, we will need to create two integration tests to fully cover everything than can occur when our getResults() method is invoked. First we need to test that when the user supplies a good query, that a list of results is returned. Later, we will test that when the user supplies a bad query that status code of 404 is returned.

To start, lets create a method to hold our happy path test.

@Test
public void givenGoodQuery_whenSearchForResults_thenIsOkAndReturnsResults() throws Exception {
    //given
    //when
    //then
}

Stubbing the LocService Method

We've created our mocked LocService with the previous steps, but we still need to program it to return a specified value when locService.getResults() is invoked. This is done in exactly the same way as it was in LocControllerTest, and you may in fact be able to borrow some code from that class to complete this one.

@Test
public void givenGoodQuery_whenSearchForResults_thenIsOkAndReturnsResults() throws Exception {
    //given
    String query = "Java";
    String title = "Java: A Drink, an Island, and a Programming Language";
    String author = "AUTHOR";
    String link = "LINK";
    Result result = new Result();
    result.setTitle(title);
    result.setAuthors(Collections.singletonList(author));
    result.setLink(link);
    List<Result> expectedResults = Collections.singletonList(result);

    when(locService.getResults(query)).thenReturn(expectedResults);

    //when
    //then
}

A Couple Initial Assertions

We will improve our test in the next step, but for now we can simply check that when a well-formatted request is sent to the /searchLocResults endpoint, a 200 status code is returned and that the content type of the response is APPLICATION_JSON.

@Test
public void givenGoodQuery_whenSearchForResults_thenIsOkAndReturnsResults() throws Exception {
    //given
    String query = "Java";
    String title = "Java: A Drink, an Island, and a Programming Language";
    String author = "AUTHOR";
    String link = "LINK";
    Result result = new Result();
    result.setTitle(title);
    result.setAuthors(Collections.singletonList(author));
    result.setLink(link);
    List<Result> expectedResults = Collections.singletonList(result);

    when(locService.getResults(query)).thenReturn(expectedResults);

    //when
    //then
    MvcResult mvcResult = mockMvc.perform(get("/searchLocResults?q=" + query))
            .andDo(print())
            .andExpect(status().isOk())
            .andReturn();

    assertEquals(MediaType.APPLICATION_JSON_VALUE, mvcResult.getResponse().getContentType());
}

This code should look pretty familiar from what we did in the HomeControllerIntTest, and if you run this test you should see it pass.

Completing the Happy Path Test

You may have noticed that, at this point, we aren't actually testing that the result that is sent in response to the user's request is what we expect. This, in theory, is the same type of thing we did while unit testing: invoke a method, then check that result is what we expected. However within our integration test, the syntax is different. In our unit tests, we were still dealing with Java objects, and we directly tested the object returned from calling a method was what we expected. In our integration test, the response we receive does not contain a Java object, but rather the JSON value that Spring has created from the Java object returned from the locController.getResults() method. Think of when we test our endpoint using Swagger: we don't receive a Java object even though the getResults() method itself returns a Java object, but instead see the result in JSON format.

It may be helpful to visit our Swagger page and send a request to the application, so you can see what the result actually looks like. In our assertion methods, we will need to pull pieces from the JSON result and check that they contain the value that we expect. In the case of testing the LocController getResults() method, it will look like this:

@Test
public void givenGoodQuery_whenSearchForResults_thenIsOkAndReturnsResults() throws Exception {
    //given
    String query = "Java";
    String title = "Java: A Drink, an Island, and a Programming Language";
    String author = "AUTHOR";
    String link = "LINK";
    Result result = new Result();
    result.setTitle(title);
    result.setAuthors(Collections.singletonList(author));
    result.setLink(link);
    List<Result> expectedResults = Collections.singletonList(result);

    when(locService.getResults(query)).thenReturn(expectedResults);

    //when
    //then
    MvcResult mvcResult = mockMvc.perform(get("/searchLocResults?q=" + query))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].title", is(title)))
            .andExpect(jsonPath("$[0].authors[0]", is(author)))
            .andExpect(jsonPath("$[0].link", is(link)))
            .andReturn();

    assertEquals(MediaType.APPLICATION_JSON_VALUE, mvcResult.getResponse().getContentType());
}

If you are attempting to write an integration test for a method that returns a different type of object, include one that may have as a field an array of values, you can look up "JSONPath syntax", or visit this site

This test is now complete, and if you run it, you should see it pass.

Our Final Piece of Code

We finally made it to the final step in creating our application! All that is left is to test the unhappy path for our getResults() method. Just like in our unit test, this is actually quite simple since the default value returned from a mocked method is an empty list, which is eactly the behavior we are looking for to trigger the 404 status code being returned from our method.

@Test
public void givenBadQuery_whenSearchForResults_thenIsNotFound() throws Exception {
    //given
    String query = "Java";

    //when
    //then
    mockMvc.perform(get("/searchLocResults?q=" + query))
            .andDo(print())
            .andExpect(status().isNotFound());
}

Running Our Tests One More Time

Before we call the application complete, run all of the unit tests and integration tests one more time to ensure that they all pass. You can do this in IntelliJ by using the Gradle tab on the right side of the IDE, then Cheetah-Search>Tasks>Verification>check. You could also run them in terminal using the command "gradle check", but the output is not quite as nice to read. If you have 8 tests passing, then congratulations!

Summary of Code Changes for this Milestone

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