Upcoming courses: | ||
---|---|---|
8th, 10th, 15th and 17th of February 2022 | Twice a week - 2 Tuesdays and 2 Thursdays | Register Now! |
We’ve gone over what we needed from Spring to test a controller. And Spring is about to give it to us.
Here’s a controller:
@RestController()
@RequestMapping("/items/")
public class ItemController {
@Autowired ItemRepository itemRepository;
@GetMapping(value ="/")
public ResponseEntity<String> getAllItems() {
Integer numberOfItems = itemRepository.findTotal();
if (numberOfItems == 0)
return new ResponseEntity<String>("Error", HttpStatus.SERVICE_UNAVAILABLE);
else {
String result =numberOfItems.toString() + " Items";
return new ResponseEntity<String> (result , HttpStatus.OK);
}
}
@PostMapping(value = "add/")
public ResponseEntity<?> addItem(@RequestBody String itemName) {
Item item = new Item(itemName, 1);
itemRepository.addItem(item);
return new ResponseEntity(HttpStatus.OK);
}
}
Code language: JavaScript (javascript)
This controller has GET and POST APIs. It has some logic in it (although this is something we probably don’t want. We’d rather push that into our application logic layer). It also calls a Repository (it doesn’t matter which type, JDCB or JPA.) While we can just inject it into our integration tests, and call its methods directly, we’d rather call the APIs as a URI.
Remember: what we’ve wanted all along was a standalone server, that from its entry points onward, runs our code. That server can be called with an API and return an HTTP code and payload.
Spring comes with a handy class called MvcTestController. It is basically a web server that lives for the duration of the integration test run. It is limited to exposing only the API of the controllers-under-test, so not all the application loads. Our wish has comes true, with a bonus.
In order to create it, we have a couple of ways. The first one is to use the WebApplicationContext, which Spring creates and injects automatically.
@Autowired WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.build();
controller.Reset();
}
Code language: JavaScript (javascript)
The mockMvc is created for each integration test. But even that maybe too much, so the good people of Spring thought of us lazy bastards, and gave us an annotation for our integration test class:
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes= {TestControllerConfiguration.class})
@AutoConfigureMockMvc
public class ControllerTests {
@Autowired MockMvc mockMvc;
...
}
Code language: CSS (css)
@AutoConfigureMockMvc basically injects mockMvc with less coding. So what can this thing do?
@Test
public void whenNoItemsAvailable_thenGetReturnsAnErrorCode() throws Exception {
mockMvc.perform(get("/items/"))
.andExpect(status().isServiceUnavailable());
}
Code language: PHP (php)
TestMockMvc comes packed with a fluent interface to do all the HTTP operations (here we do a HTTP GET) and asserts if we got the right status. It can do a lot more though: We can pass in headers, request bodies, authorization data. And we can return all kind of statuses and check the response body. Here’s another integration test:
@Test
public void whenItemsAreAdded_thenGetReturnsNumberOfItems() throws Exception {
mockMvc.perform(post("/items/add/")
.content(asJsonString("Item1"))
.contentType("application/json"))
.andExpect(status().isOk());
mockMvc.perform(post("/items/add/")
.content(asJsonString("Item2"))
.contentType("application/json"))
.andExpect(status().isOk());
MvcResult result = mockMvc.perform(get("/items/"))
.andExpect(status().isOk())
.andReturn();
assertEquals("2 Items",result.getResponse().getContentAsString());
}
Code language: PHP (php)
Here’s the JSON serialization method, used to wrap the strings:
private String asJsonString(Object obj) {
try {
return new ObjectMapper().writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Code language: JavaScript (javascript)
As you can see, the TestMockMvc allows us to operate and check the controller as if it was called from the outside. While you might be tempted to write all your tests like this, remember that the longer the test path, the integration test can fail for many reasons.
There’s another downside: integration tests tend to get longer and longer, and therefore hard to read and maintain. Which makes it very important to extract those repeated calls into methods and leave the integration test like this:
@Test
public void api_whenItemsAreAdded_thenGetReturnsNumberOfItems() throws Exception {
addItem("Item1");
addItem("Item2");
assertEquals("2 Items",getTotal());
}
Code language: PHP (php)
Much better, right? That’s how we like our integration tests.
So that’s the TestMockMvc. What I’d like next is to mock a full API call. Can I do that? Of course I can. Next time.
0 Comments