Unit testing REST controllers

We have just tested a traditional controller redirecting to a view. Testing a REST controller is very similar in principle, but there are a few subtleties.

Since we are going to test the JSON output of our controller, we need a JSON assertion library. Add the following dependency to your build.gradle file:

testCompile 'com.jayway.jsonpath:json-path'

Let's write a test for the SearchApiController class, the controller that allows searching for a tweet and returns results as JSON or XML:

package masterSpringMvc.search.api;

import masterSpringMvc.MasterSpringMvcApplication;
import masterSpringMvc.search.StubTwitterSearchConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {
        MasterSpringMvcApplication.class,
        StubTwitterSearchConfig.class
})
@WebAppConfiguration
public class SearchApiControllerTest {
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    @Test
    public void should_search() throws Exception {

        this.mockMvc.perform(
                get("/api/search/mixed;keywords=spring")
                        .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].text", is("tweetText")))
                .andExpect(jsonPath("$[1].text", is("secondTweet")));
    }
}

Note the simple and elegant assertions on the JSON output. Testing our user controller will require a bit more work.

First, let's add assertj to the classpath; it will help us write cleaner tests:

testCompile 'org.assertj:assertj-core:3.0.0'

Then, to simplify testing, add a reset() method to our UserRepository class that will help us with the test:

void reset(User... users) {
        userMap.clear();
        for (User user : users) {
                save(user);
        }
}

In real life, we should probably extract an interface and create a stub for testing. I will leave that as an exercise for you.

Here is the first test that gets the list of users:

package masterSpringMvc.user.api;

import masterSpringMvc.MasterSpringMvcApplication;
import masterSpringMvc.user.User;
import masterSpringMvc.user.UserRepository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.Matchers.*;
   import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MasterSpringMvcApplication.class)
@WebAppConfiguration
public class UserApiControllerTest {

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private UserRepository userRepository;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
        userRepository.reset(new User("[email protected]"));
    }

    @Test
    public void should_list_users() throws Exception {
        this.mockMvc.perform(
                get("/api/users")
                        .accept(MediaType.APPLICATION_JSON)
        )
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$", hasSize(1)))
                .andExpect(jsonPath("$[0].email", is("[email protected]")));
    }
}

For this to work, add a constructor to the User class, taking the e-mail property as a parameter. Be careful: you also need to have a default constructor for Jackson.

The test is very similar to the previous test with the additional setup of UserRepository.

Let's test the POST method that creates a user now:

import static org.assertj.core.api.Assertions.assertThat;

// Insert this test below the previous one
@Test
public void should_create_new_user() throws Exception {
        User user = new User("[email protected]");
        this.mockMvc.perform(
                post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(JsonUtil.toJson(user))
        )
                .andExpect(status().isCreated());

        assertThat(userRepository.findAll())
                .extracting(User::getEmail)
                .containsOnly("[email protected]", "[email protected]");
}

There are two things to be noted. The first one is the use of AssertJ to assert the content of the repository after the test. You will need the following static import for that to work:

import static org.assertj.core.api.Assertions.assertThat;

The second is that we use a utility method to convert our object to JSON before sending it to the controller. For that purpose, I created a simple utility class in the utils package, as follows:

package masterSpringMvc.utils;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public class JsonUtil {
    public static byte[] toJson(Object object) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return mapper.writeValueAsBytes(object);
    }
}

The tests for the DELETE method are as follows:

@Test
public void should_delete_user() throws Exception {
        this.mockMvc.perform(
                delete("/api/user/[email protected]")
                        .accept(MediaType.APPLICATION_JSON)
        )
                .andExpect(status().isOk());

        assertThat(userRepository.findAll()).hasSize(0);
}

@Test
public void should_return_not_found_when_deleting_unknown_user() throws Exception {
        this.mockMvc.perform(
                delete("/api/user/[email protected]")
                        .accept(MediaType.APPLICATION_JSON)
        )
                .andExpect(status().isNotFound());
}

Finally, here's the test for the PUT method, which updates a user:

@Test
public void put_should_update_existing_user() throws Exception {
        User user = new User("[email protected]");
        this.mockMvc.perform(
                put("/api/user/[email protected]")
                        .content(JsonUtil.toJson(user))
                        .contentType(MediaType.APPLICATION_JSON)
        )
                .andExpect(status().isOk());

        assertThat(userRepository.findAll())
                .extracting(User::getEmail)
                .containsOnly("[email protected]");
}

Whoops! The last test does not pass! By checking the implementation of UserApiController, we can easily see why:

   @RequestMapping(value = "/user/{email}", method = RequestMethod.PUT)
    public ResponseEntity<User> updateUser(@PathVariable String email, @RequestBody User user) throws EntityNotFoundException {
        User saved = userRepository.update(email, user);
        return new ResponseEntity<>(saved, HttpStatus.CREATED);
    }

We returned the wrong status in the controller! Change it to HttpStatus.OK and the test should be green again.

With Spring, one can easily write controller tests using the same configuration of our application, but we can just as efficiently override or change some elements in our testing setup.

Another interesting thing that you will notice while running all the tests is that the application context is only loaded once, which means that the overhead is actually very small.

Our application is small too, so we did not make any effort to split our configuration into reusable chunks. It can be a really good practice not to load the full application context inside of every test. You can actually split the component scanned into different units with the @ComponentScan annotation.

This annotation has several attributes that allow you to define filters with includeFilter and excludeFilter (loading only the controller for instance) and scan specific packages with the basePackageClasses and basePackages annotations.

You can also split your configuration into multiple @Configuration classes. A good example would be splitting the code for the users and for the tweet parts of our application into two independent parts.

We will now have a look at acceptance tests, which are a very different kind of beast.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.61.81