What You'll Learn

We've started writing automated tests. We now want to explore more options. Can we automate tests of page requests or API calls? Can we write tests that are independent of the database, yet still test the controllers?

To demonstrate the testing features (and to correct a few missing scenarios), there are some changes to the application code.

Controllers

Changing the design of a GET request response

In CharitySearchAPIController.java

@GetMapping("charity")
public ResponseEntity<List<CharityDTO>> findAll(@RequestParam(value = "search", required = false) Optional<String> searchTerm) {

    List<CharityDTO> charityDTOList;
    if (searchTerm.isPresent()) {
        charityDTOList = charitySearch.findBySearchTerm(searchTerm.get());
    } else {
        charityDTOList = charitySearch.findAll();
    }

    return ResponseEntity.ok(charityDTOList);
}

In this method, we've adopted the view that if the parameter is left off the query, then we want to return all charities. If we don't check for this, then the query would return no charities.

Changing the response on a POST

We've added a method to handle a POST request. After checking for advice on REST conventions, the design returns a 201 (Created) and returns the URL to the new resource.

In CharitySearchAPIController.java

@PostMapping("/charity")
ResponseEntity newCharity(@RequestBody CharityDTO newCharity, UriComponentsBuilder uriBuilder) {

    charitySearch.saveCharity(newCharity);// let's assume it works.
    UriComponents uriComponents = uriBuilder.path("/api/charity/{id}").buildAndExpand(newCharity.getFurl());

    ResponseEntity responseEntity = ResponseEntity.created(uriComponents.toUri()).build();
    return responseEntity;

}

The UriComponents provide utility methods to create useful URLs.

Database Initialisation Scripts

We have renamed the scripts to schema-test-h2.sql and data-test-h2.sql just to make it clearer to the reader that these scripts are for testing purposes. We may want to have separate files for demo purposes that still use H2. We'll see in a future tutorial for Spring Boot provides the "profiles" feature to make selection of scripts easier.

Continuing with the approach from before, we'll now write some full container tests, but we'll trigger the test with web requests rather than method calls (i.e. we'll stimulate the controllers rather than the service layer).

Amend the test class

We need to introduce some extra annotations on the class.

In CharitySearches.java (in the fullcontainer package)

@SpringBootTest
@AutoConfigureMockMvc
@Sql({"/schema-test-h2.sql", "/data-test-h2.sql"})
public class CharitySearches {

  @Autowired
  private CharitySearch charityService;

  @Autowired
  MockMvc mvc;

We know that @SpringBootTest means that a full container will be created.

The new annotation @AutoConfigureMockMvc tells the tests to create a mock MVC component. This mock allows us to simulate a web request.

In normal testing, a browser would be directed to a web address and the request would be handled by a running server. That server directs the request to the framework that directs the request to the required method in a controller. This requires a network call to be made (by the browser) and network calls to be listened for by a server.

In that journey, the server/framework combination turn HTTP requests into objects that represent those requests.

With a mock MVC, we can create those same abstractions and fire them into the framework without the overhead of network calls and handling.

The @Sql annotation allows the test developer to specify which SQL scripts should be run before the tests are invoked.

The service is then autowired in as is a MockMvc bean. This is the entry point for these mock web calls. Tests will typically use the perform() method to create different types of requests.

Add a test method for a GET request

We want to test that our API is returning correct JSON data.

In CharitySearches.java (in the fullcontainer package)

@Test
public void shouldGet11CharitiesAsJSON() throws Exception{
    mvc.perform(get("/api/charity").contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(11)));
}

This has a weak assertion as it only checks the size of the JSON array, but it starts the learning journey.

The test uses the injected mvc object and calls its perform method. We ask it to perform a get request to the ‘/api/charity' URL and specify the content type as JSON.

The andDo(print()) call will print the test output to the console.

We then have two expected results; that the HTTP response code is 200 (OK) and that the root ($) JSON object has a size of 11.

With these basics in our testing toolkit, we can combine them with other testing approaches to test other URLs. For example, we can parameterise the tests on our searches.

In CharitySearches.java (in the fullcontainer package)

@ParameterizedTest
@CsvSource({"heart,1","cancer, 3", "dogs, 0", "cats, 2"})
public void shouldGetNCharitiesFromSearchAsJSON(String search, String countAsString) throws Exception{
    Integer count = Integer.valueOf(countAsString);
    mvc.perform(get("/api/charity?search="+search).contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(count)));
}

In this example, we're using @ParameterizedTest and @CsvSource. The test will substitute each pair; the first string as the search term, and the 2nd string will be converted to an integer and is the expected size.

We can do similar on the API to get a single charity and check the response code.

In CharitySearches.java (in the fullcontainer package)

@ParameterizedTest
    @CsvSource({"bhf,200","cruk, 200", "dogstrust, 404", "cpl, 404"})
    public void shouldGetRightStatusFromAPIAsJSON(String furl, String status) throws Exception{

        int statusCode = Integer.valueOf(status);

        mvc.perform(get("/api/charity/"+furl).contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().is(statusCode))
                ;
    }

Each test provides the acronym and expected response code.

We can also test post requests that create new resources (objects)?

Create a new class called CharityCreates.java in the same fullcontainer folder.

In CharityCreates.java

@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class CharityCreates {

    @Autowired private CharitySearch charityService;

    @Autowired MockMvc mvc;

    private static final ObjectMapper om = new ObjectMapper();


    @Test
    public void shouldBeAbleToPostNewCharity() throws Exception {

        CharityDTO charityDTO = new CharityDTO(null,
                                               "A new charity",
                                               "000000",
                                               "new",
                                               "new",
                                               "a typically bland mission statement",
                                               true);

        mvc.perform(post("/api/charity")
                                .content(om.writeValueAsString(charityDTO))
                                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", containsString("/api/charity/new")));
    }
}

The @DirtiesContext annotation tells the testing framework that the tests in this class will change the test context. Since we don't know the order in which the tests will run (and we wouldn't want to order them anyway), we need to reset the context after each test so that they are starting from a known state.

In the test, we create a CharityDTO object and then use perform to post the object as JSON. We use an ObjectMapper object to convert our DTO into JSON.

The expected outputs are a status of 201 (created) and for the "Location" field in the HTTP response header to be the URL of the created charity.

Run the Tests, either in IntelliJ or via Gradle > Build > build.

We've expanded our testing capability to test our application end-to-end using a mock container that simulates the receipt of HTTP requests. However, the tests aren't that fast and any test failures will be difficult to diagnose. There are options that will help us focus our testing.