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
- java
- org.jointheleague.level7.cheetah
- presentation
- HomeControllerIntTest
- LocControllerIntTest.java
- 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