13) Spring Test Lesson

Testing Controllers with MockMvc

15 min to complete · By Ryan Desmond, Jared Larsen

The last chapter gave you an idea of how to create and augment the environment in which your tests run. Now it's time actually to write tests for your application. This lesson will cover testing your MVC controller endpoints.

Example Location:
com.codingnomads.springtest.usingmockmvc

What Are You Testing?

The above phrase "testing your controller endpoints" can mean two different things:

  1. You can do a top-down test from controllers to the database (integration testing).
  2. You can mock the downstream components and test your controller classes as units (unit testing).

Depending on whether you are mocking the downstream components or not, you can use @SpringBootTest or @WebMvcTest, respectively. Remember that @SpringBootTest starts all of Spring, while @WebMvcTest starts only the web layer. This lesson will show you how to write integration tests for your controllers.

Testing Controllers using MockMvc

Before you can start testing an application, you need an application. Here is a quick setup that returns either a greeting view or "Hello Back" in the HTTP body.

@RequestMapping("/")
@Controller
public class HomeController {

    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("name", "Bobbert");
        return "greeting";
    }

    @GetMapping("/hello")
    @ResponseBody
    public String greet() {
        return "Hello Back";
    }
}

Now that there is an application to test, a test class like the one below is needed to ensure that the endpoints are working as intended. Pay special attention to the use of MockMvc and the comments. Don't worry if this is overwhelming. An explanation is on the way.

// tell Spring to start completely and indicate the bootstrapping class
@SpringBootTest(classes = MockMvcMain.class)
// indicate that Spring should autoconfigure the MockMvc object
@AutoConfigureMockMvc
public class TestWebServices {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void helloShouldReturnDefaultMessage() throws Exception {
        // use mockMvc to start a request
        mockMvc
            // indicate what MockMvc should do
            .perform(
                    // the get method and path for the request
                    get("/hello"))
            // print the response
            .andDo(print())
            // the response should have status 200 OK
            .andExpect(status().isOk())
            // expect the response body to contain "Hello Back"
            .andExpect(content().string(containsString("Hello Back")));
    }

    @Test
    public void baseURLShouldReturnGreetingViewName() throws Exception {
        // use MockMvc to start a request
        mockMvc
            // indicate the HTTP method and url path
            // to be used to make the request
            .perform(get("/"))
            // the result should be printed
            .andDo(print())
            // the view name expected is greeting
            .andExpect(view().name("greeting"));
    }
}

Hopefully, the comments helped you to demystify this new MockMvc object and its many possibilities. Regardless, here's a more detailed explanation of what's going on.

  1. Notice the @AutoConfigureMockMvc annotation and the private MockMvc variable declaration. @AutoConfigureMockMvc lets Spring know you want it to set up the MockMvc object for you. It can then be autowired in anywhere you need it.

  2. Do you see how mockMvc is immediately used to call the perform() method in both tests? perform() is the starting point for testing an endpoint using MockMvc. It takes a RequestBuilder parameter, indicating that everything passed into perform() is used to format/build the request.

To create a complete RequestBuilder object to pass into the perform() method, you start by calling one of these methods:

  • get(): to indicate the GET HTTP method should be used
  • post(): to use POST
  • put(): to use PUT
  • patch(): to use PATCH
  • delete(): to use DELETE
  • options(): to use OPTIONS
  • head(): to use HEAD
  • request(): to specify a custom HTTP method
  • multipart(): to indicate a multipart request
Illustration of a lighthouse

Info: Each method takes in a URL to indicate where the request should be made, but if the request is local, you can ignore most of the URL and include only the path and query parameters.

To format the request further, you use a builder pattern. Like so:

perform(post("/path/to/endpoint")
    // add some data to the request body
    .content("this is added to the body of the request")
    // add a content type header
    .contentType("text/plain")
    // indicate the preferred response datatype 
    .accept("application/json")
    // add a custom header
    .header("headerName","headerValue")
    // indicate whether HTTP or HTTPS should be used
    .secure(true))

Now that you've seen how to use a RequestBuilder to format your request, it's time to use the rest of the MockMvc API to test the response. This is centered around the ResultAction class's three methods:

  1. andDo() takes in a ResultHandler object and performs whatever the ResultHandler requests. You can pass in things like print() and log().
  2. andExpect() takes in a ResultMatcher and compares the expectation to the actual result. This is where most of the testing happens.
  3. andReturn() returns an MvcResult object containing the result of the request. This affords you direct access to the response without using andExpect(). If you can't test something the way you want using andExpect() this is a backup option.

All three methods are useful for testing, but andExpect() deserves a bit more attention.

Testing Expectations

Using the andExpect() method can be a bit complicated. There are a lot of things to test, and a lot of ways to test them. Here is a list of the available things to test:

  • jsonPath(): access parts of the JSON response body
  • status(): confirm the HTTP status returned by the server
  • header(): check if a response header is present and contains the correct value
  • content(): access the body of the request and its meta-data
  • request(): confirm information about the request
  • cookie(): access any cookies that are returned from the server
  • handler(): information about the handlers that were tasked with handling this request
  • model(): check information from the returned model
  • view(): make sure the view matches certain expectations
  • forwardedUrl(): check the URL where the request was forwarded.
  • redirectedUrl(): confirm the possible redirect URL

This is an extensive list, and each method exposes a builder-like pattern to confirm values. This lesson would be extremely long if it listed every available option for the methods listed. Instead, it will be more helpful to look at some examples. Follow along with these tests for hypothetical endpoints and their expected responses.

Confirm a 404 NOT FOUND

mockMvc
        // set up a GET request to a non-existent endpoint
        .perform(get("/path/that/does/not/exist"))
        // expect response status 404 NOT FOUND
        .andExpect(status().isNotFound())
        // expect JSON to be returned
        .andExpect(content().contentType("application/json"));

Confirm the Correct View and Model

mockMvc
        // set up a GET request to an endpoint that returns a model
        .perform(get("should/return/view/with/name/settings"))
        // expect the response status is 200 OK
        .andExpect(status().isOk())
        // expect the content type to be HTML
        .andExpect(content().contentType("text/html;charset=UTF-8"))
        // expect the view name to be settings
        .andExpect(view().name("settings"))
        // expect the model to include an attribute value 
        .andExpect(model().attribute("username", "expectedUsername"));

Confirm Custom Header Is Present

mockMvc
        // create POST request
        .perform(post("/custom/header/should/be/added")
        // add data to the request body
        .content("some content")
        // make the request HTTPS
        .secure(true))
        // expect 200 OK
        .andExpect(status().isOk())
        // test if it is present
        .andExpect(header().exists("Custom-Header"))
        // test its value also
        .andExpect(header().string("Custom-Header", "headerValue"));

Confirm the Body of a Response is Empty

mockMvc.perform(get("/custom/header/should/be/added"))
        .andExpect(status().isOk())
        .andExpect(content().string(emptyString()));

Confirm Redirect URL

mockMvc
        // create GET request
        .perform(get("/custom/header/should/be/added")
                .header("header name", "header value"))
        // expect status 308 PERMANENT REDIRECT
        .andExpect(status().isPermanentRedirect())
        // check Location header using redirectUrl()
        .andExpect(redirectedUrl("REDIRECT URL"))
        // manual redirect header check
        .andExpect(header().string("Location", "REDIRECT URL"));

MockMvc Limitation

MockMvc is a great tool for integration testing of endpoints and controllers in your Spring applications. When it comes to directly testing service layer methods or other components not exposed as web endpoints, MockMvc isn't the right tool. For these, traditional unit testing approaches are more appropriate.

Testing the Testing

If your experience with Java testing is limited, you may be ready to run this example wondering, "where is this elusive TestWebServices class?" Test classes are located in the src/test package rather than src/main.

This is the class you will run to execute the tests. You will see a lot of debug info print to the console, and if all tests pass you will end up with something like:

If a test fails, you can expect something similar:

java.lang.AssertionError: Response content
Expected: a string containing "Hello Back"
     but: was "Hello Bck"

Learn by Doing

Package (main): com.codingnomads.springtest.usingmockmvc
Package (test): com.codingnomads.usingmockmvc

  • Inside HomeController, create at least three new controller methods, each of which should be tested using a different technique from the "A Few More Examples" section above.
  • Implement these testing methods inside TestWebServices, and ensure that all of your tests pass.

Be sure to push your work to GitHub when you're done.

Summary: MockMvc

This lesson was a lot. After reading, you might not be able to put complex MockMvc tests together easily, but you are headed in the right direction. Testing requires a lot of practice before it becomes anything close to "easy".

With that said, here is a quick summary:

  • To use MockMvc, you can use the @AutoConfigureMockMvc annotation and autowire a MockMvc reference into your test class.
  • MockMvc starts with the perform method. Perform takes in a RequestBuilder that is used to edit the request to be sent to the server. After the request is sent, there are three methods available to access/test the response:
    • andDo(): do something with the result
    • andExpect(): compare the actual to the expected
    • andReturn(): get direct access to the MvcResult
  • andExpect() has a significant number of possible inputs used to test the results. Hopefully, the examples helped give you a better idea of how it is used.