What You'll Learn

The application architecture has many layers; templates > controllers > services > repositories > database.

Our tests currently require the full application to be available. If a test fails, then the issue could lie anywhere in the architecture.

If we're testing that a page shows the correct data, or consists of the correct mark-up - or if we're testing the data returned by an API, then we can focus on testing on the MVC layer, by using mocks for the layers below.

If we can fix the behaviour of the service layer within the context of a test, then we know that that dependency is behaving as expected so any failures must be present in the components that actually in play.

For example, if we want to test a request to "/api/charity/nspcc", we expect the the service layer will be called with the acronym parameter set to "nspcc". If the controller doesn't do that, then we'd expect the test to fail.

Further, given that we expect that service call, we can define the contract for that call. We can "fix" the return value (in this case, an Optional DTO) to one that we create purely for the purposes of the test.

And further again, if we can expect that "fixed" DTO to come back, we can expect the JSON to contain certain data.

To fix these parts of the call stack, we can replace our normal components with mocks and set expectations on them. Spring will inject these mocks rather than the real components in these tests and therefore, we can test the controllers with confidence.

We will put mock tests in a separate package called mockcontainer. Let's create a class called CharitySearchesMock.java.

In CharitySearchesMock.java

@WebMvcTest(controllers={uk.ac.cf.cs.cm6213.tutorialcompanion.api.controllers.CharitySearchAPIController.class})
@AutoConfigureMockMvc
public class CharitySearchesMock {
  @MockBean
  private CharitySearch charityService;

  @Autowired
  MockMvc mvc;

Notice that we're not using @SpringBootTest here. We've replaced that with @WebMvcTest. This will start a Spring container, but only with enough context to support controllers. We specify the controllers to load into the context on the annotation.

We inject the mock MVC object as before and we also ask Spring to provide a mocked version of our CharitySearch bean using @MockBean.

We can then code a test.

In CharitySearchesMock.java

@Test
   public void shouldGet5CharitiesAsJSON() throws Exception{

       CharityDTO charityDTO1 = new CharityDTO(1L,"Charity1", "123456","c1","c1","mission statement",true);
       CharityDTO charityDTO2 = new CharityDTO(2L,"Charity2", "123456","c2","c2","mission statement",true);
       CharityDTO charityDTO3 = new CharityDTO(3L,"Charity3", "123456","c3","c3","mission statement",true);
       CharityDTO charityDTO4 = new CharityDTO(4L,"Charity4", "123456","c4","c4","mission statement",true);
       CharityDTO charityDTO5 = new CharityDTO(5L,"Charity5", "123456","c5","c5","mission statement",true);

       List<CharityDTO> charities = List.of(charityDTO1, charityDTO2, charityDTO3, charityDTO4, charityDTO5);

       given(charityService.findAll()).willReturn(charities);


       mvc.perform(get("/api/charity").contentType(MediaType.APPLICATION_JSON))
               .andDo(print())
               .andExpect(status().isOk())
               .andExpect(jsonPath("$", hasSize(5)));

   }

In this example, we create 5 charity DTO objects, put them into a list and then use the mocking features of the Mockito framework.

given(charityService.findAll()).willReturn(charities);

This line sets an expectation on the mock. It says that when the mock's findAll() method is called, we expect it to return the fixed list of charity DTO objects.

We can then code a normal "perform" test and we can expect the size to be 5 (not 11 as are in the database since this test doesn't access the database).

We can do the same with parameters. In this test, we create a DTO with an acronym of "c2" and perform a get request to "/api/charity/c2". "c2" doesn't exist in the database, so to work, the test must be using the mock.

Our mock can only support a method call to findByAcronym() with the parameter value of "c2" so if the controller doesn't call that correctly, the test will fail.

Once the mock returns, we can make assertions on the controller as normally as we would with a full stack test.

If the test fails, then we can focus on the controller or template. We may need to check if the expectations are correct (which may then ask questions around documentation, etc) but those are easier to fix and focus can then return to the more likely problem areas.

Again, you can run the test using IntelliJ or Gradle.

We've introduced mocking which speeds up testing (since less code has to be loaded by Spring), reduces the moving parts so that the tests become more useful and forces the developer to really think about the contracts between the components.