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:
- You can do a top-down test from controllers to the database (integration testing).
- 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.
-
Notice the
@AutoConfigureMockMvcannotation and the private MockMvc variable declaration.@AutoConfigureMockMvclets Spring know you want it to set up the MockMvc object for you. It can then be autowired in anywhere you need it. -
Do you see how
mockMvcis immediately used to call theperform()method in both tests?perform()is the starting point for testing an endpoint using MockMvc. It takes a RequestBuilder parameter, indicating that everything passed intoperform()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 usedpost(): to use POSTput(): to use PUTpatch(): to use PATCHdelete(): to use DELETEoptions(): to use OPTIONShead(): to use HEADrequest(): to specify a custom HTTP methodmultipart(): to indicate a multipart request
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:
andDo()takes in a ResultHandler object and performs whatever the ResultHandler requests. You can pass in things likeprint()andlog().andExpect()takes in a ResultMatcher and compares the expectation to the actual result. This is where most of the testing happens.andReturn()returns an MvcResult object containing the result of the request. This affords you direct access to the response without usingandExpect(). If you can't test something the way you want usingandExpect()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 bodystatus(): confirm the HTTP status returned by the serverheader(): check if a response header is present and contains the correct valuecontent(): access the body of the request and its meta-datarequest(): confirm information about the requestcookie(): access any cookies that are returned from the serverhandler(): information about the handlers that were tasked with handling this requestmodel(): check information from the returned modelview(): make sure the view matches certain expectationsforwardedUrl(): 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
@AutoConfigureMockMvcannotation 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 resultandExpect(): compare the actual to the expectedandReturn(): 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.