I'm running unit tests with Spring Boot and Mockito and I'm testing RESTful services. When I try to test the GET method it works successfully but when I try to test the POST method it fails. What should I do to resolve this problem? Thanks in advance!
This is the code for the REST Controller:
package com.dgs.restfultesting.controller;
import java.net.URI;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.dgs.restfultesting.business.ItemBusinessService;
import com.dgs.restfultesting.model.Item;
#RestController
public class ItemController {
#Autowired
private ItemBusinessService businessService;
#GetMapping("/all-items-from-database")
public List<Item> retrieveAllItems() {
return businessService.retrieveAllItems();
}
#PostMapping("/items")
public Item addItem(#RequestBody Item item) {
Item savedItem = businessService.addAnItem(item);
return savedItem;
}
}
Business Layer:
package com.dgs.restfultesting.business;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.dgs.restfultesting.data.ItemRepository;
import com.dgs.restfultesting.model.Item;
#Component
public class ItemBusinessService {
#Autowired
private ItemRepository repository;
public Item retrieveHardcodedItem() {
return new Item(1, "Book", 10, 100);
}
public List<Item> retrieveAllItems() {
List<Item> items = repository.findAll();
for (Item item : items) {
item.setValue(item.getPrice() * item.getQuantity());
}
return items;
}
public Item addAnItem(Item item) {
return repository.save(item);
}
}
ItemControllerTest:
package com.dgs.restfultesting.controller;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Arrays;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import com.dgs.restfultesting.business.ItemBusinessService;
import com.dgs.restfultesting.model.Item;
#RunWith(SpringRunner.class)
#WebMvcTest(ItemController.class)
public class ItemControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private ItemBusinessService businessService;
#Test
public void retrieveAllItems_basic() throws Exception {
when(businessService.retrieveAllItems()).thenReturn(
Arrays.asList(new Item(2, "iPhone", 1000, 10),
new Item(3, "Huawei", 500, 17)));
RequestBuilder request = MockMvcRequestBuilders
.get("/all-items-from-database")
.accept(MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().json("[{id:2, name:iPhone, price:1000}, {id:3, name:Huawei, price:500}]")) // This will return an array back, so this data should be within an array
.andReturn();
}
#Test
public void createItem() throws Exception {
RequestBuilder request = MockMvcRequestBuilders
.post("/items")
.accept(MediaType.APPLICATION_JSON)
.content("{\"id\":1,\"name\":\"Book\",\"price\":10,\"quantity\":100}")
.contentType(MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(request)
.andExpect(status().isCreated())
.andExpect(header().string("location", containsString("/item/")))
.andReturn();
}
}
There is no problem to test the retrieveAllItems_basic() method, but when I try to run a JUnit test for createItem() method, it doesn't work and I get this: java.lang.AssertionError: Status expected:<201> but was:<200>
And this is what I receive in the Console:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /items
Parameters = {}
Headers = {Content-Type=[application/json], Accept=[application/json]}
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = com.dgs.restfultesting.controller.ItemController
Method = public com.dgs.restfultesting.model.Item com.dgs.restfultesting.controller.ItemController.addItem(com.dgs.restfultesting.model.Item)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = {}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
2018-10-07 17:53:51.457 INFO 55300 --- [ Thread-3] o.s.w.c.s.GenericWebApplicationContext : Closing org.springframework.web.context.support.GenericWebApplicationContext#71075444: startup date [Sun Oct 07 17:53:48 EEST 2018]; root of context hierarchy
Update -----------------------------
I try to set the location like this: item/id.
This is the code for the controller:
#PostMapping("/items")
public ResponseEntity<Object> addItem(#RequestBody Item item) {
Item savedItem = businessService.addAnItem(item);
HttpHeaders httpHeaders = new HttpHeaders();
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance();
UriComponents uriComponents =
uriComponentsBuilder.path("/item/{id}").buildAndExpand(savedItem.getId());
httpHeaders.setLocation(uriComponents.toUri());
return new ResponseEntity<>(savedItem, httpHeaders, HttpStatus.CREATED);
}
This is the code for test:
#Test
public void createItem() throws Exception {
RequestBuilder request = MockMvcRequestBuilders
.post("/items")
.accept(MediaType.APPLICATION_JSON)
.content("{\"id\":1,\"name\":\"Book\",\"price\":10,\"quantity\":100}")
.contentType(MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(request)
.andExpect(status().isCreated())
.andExpect(header().string("location", containsString("/item/1")))
.andReturn();
}
When I run the JUnit test for createItem() method I get this error: org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.NullPointerException
Return 201 from your controller: As your assertion test is expecting 201 by using created status but your controller is returning 200(OK).
#PostMapping("/items")
public ResponseEntity<?> addItem(#RequestBody Item item) {
Item savedItem = itemBusinessService.addAnItem(item);
return new ResponseEntity<>(savedItem, HttpStatus.CREATED);
}
Or modify your test to check status OK(200). Update your test if you don't want to assert "location".
#Test
public void createItem() throws Exception {
RequestBuilder request = MockMvcRequestBuilders
.post("/items")
.accept(MediaType.APPLICATION_JSON)
.content("{\"id\":1,\"name\":\"Book\",\"price\":10,\"quantity\":100}")
.contentType(MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(request)
.andExpect(status().isOk()).andReturn();
}
UPDATE--Allow location header in response
If you want "location" to return from header, then modify your controller and test case below to check location too in header.
STEP 1: In your controller's add item method, add location uri and return.
#PostMapping("/items")
public ResponseEntity<?> addItem(#RequestBody Item item) {
Item savedItem = businessService.addAnItem(item);
HttpHeaders httpHeaders = new HttpHeaders();
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance();
UriComponents uriComponents =
uriComponentsBuilder.path("/item/").buildAndExpand("/item/");
httpHeaders.setLocation(uriComponents.toUri());
return new ResponseEntity<>(savedItem, httpHeaders, HttpStatus.CREATED);
}
STEP 2: Now your test will assert "location" as you expected.
#Test
public void createItem() throws Exception {
RequestBuilder request = MockMvcRequestBuilders
.post("/items")
.accept(MediaType.APPLICATION_JSON)
.content("{\"id\":1,\"name\":\"Book\",\"price\":10,\"quantity\":100}")
.contentType(MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(request)
.andExpect(status().isCreated())
.andExpect(header().string("location", containsString("/item/")))
.andReturn();
}
first of all I do not see in your createItem test the mock program part let's say
Item item = new Item();
Item newItem = new Item();
when(businessService.addAnItem(item)).thenReturn(newItem);
and in your controller I do not see the Location header. Probably a code like below should be better:
#PostMapping("/items")
public ResponseEntity<?> addItem(#RequestBody Item item) {
Item savedItem = itemBusinessService.addAnItem(item);
return ResponseEntity.created(UriComponentsBuilder.fromHttpUrl("http://yourserver/item"));
}
I hope that this can help you
Related
I am learning to write Unit Test for SpringBoot Restcontroller , wrote this and test passes
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {FhirApp.class, TestSecurityConfiguration.class})
#AutoConfigureMockMvc
public class ObservationControllerTest {
private ObjectMapper objectMapper = new ObjectMapper();
#Autowired
private MockMvc mockMvc;
#MockBean
private ObservationService observationService;
#Test
public void createObservationResource() throws Exception {
given(observationService.createObservation(ResourceStringProvider.observationsource()))
.willReturn(responseDocument);
String jsonString = objectMapper.writeValueAsString(
ResourceStringProvider.observationsource());
mockMvc.perform(post("/Observation")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andExpect(status()
.isOk());
}
But as this and this , i am also getting empty response for response.getContentAsString() :
Mockito.when(observationService.createObservation(Mockito.any())).thenReturn(responseDocument);
String jsonString = objectMapper.writeValueAsString(ResourceStringProvider.observationsource());
MockHttpServletResponse response = mockMvc.perform(post("/Observation")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andReturn()
.getResponse();
assertThat(response.getContentAsString())
.isEqualTo(new ObjectMapper()
.writeValueAsString(responseDocument));
I already tried the solutions provided by them :
1: Using Mockito.any(String.class)
2: webEnvironment = SpringBootTest.WebEnvironment.MOCK
3: using thenCallRealMethod instead of thenReturn(responseDocument)
But unfortunately it didn't work,already tried different possibilities , I also tried using MockitoJunitRunner :
#RunWith(MockitoJUnitRunner.class)
#SpringBootTest(classes = {FhirApp.class, TestSecurityConfiguration.class})
#AutoConfigureMockMvc
public class ObservationControllerTest {
private MockMvc mockMvc;
#Mock
private ObservationService observationService;
#Mock
private RequestFilter requestFilter;
#InjectMocks
private ObservationController observationController;
#Before
public void setup() {
Resource resource = Utility.convertFromStringToFhirResource(Observation.class,ResourceStringProvider.observationResponse());
responseDocument=Utility.convertFromFhirResourceToMongoInsertibleDoc(resource);
//these line enabled for MockitoJUnitRunner only
this.mockMvc = MockMvcBuilders.standaloneSetup(observationController)
.setControllerAdvice(new FhirRuntimeException("Error Happened"))
.addFilters(requestFilter)
.build();
}
#Test
public void createObservationResource()throws Exception{
Mockito.when(observationService.createObservation(ResourceStringProvider.observationsource())).thenReturn(responseDocument);
String jsonString = objectMapper.writeValueAsString(ResourceStringProvider.observationsource());
MockHttpServletResponse response = mockMvc.perform(
post("/Observation")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andReturn()
.getResponse();
assertThat(response.getContentAsString()).isEqualTo(new ObjectMapper().writeValueAsString(responseDocument));
}
i think since not many have went through such issues, issue isn't much talked about . What could be the reason of empty response when response status is ok?
Controller code :
#RestController
#RequestMapping("/api")
public class ObservationController {
#Autowired
private ObservationService observationService;
#GetMapping("/Observation/{id}")
public ResponseEntity<Document> getObservationByID(
#RequestParam("_pretty") Optional<String> pretty,
#PathVariable("id") String id) {
Document resultDoc = observationService.getObservationById(id);
return new ResponseEntity<>(resultDoc, HttpStatus.OK);
}
#PostMapping(path = "/Observation", consumes = {"application/json", "application/fhir+json"},
produces = {"application/json", "application/fhir+json"})
public ResponseEntity<Document> createObservationResource(#RequestBody String fhirResource) {
Document fhirDoc = observationService.createObservation(fhirResource);
return new ResponseEntity<>(fhirDoc,
Utility.createHeaders(fhirDoc),
HttpStatus.CREATED);
}
//other methods
}
I realized the call to post in test should be /api/Observation , but it didn't make any difference . Thanks in Advance.
Posting the complete answer alongwith imports here so that it will help someone (actually we don't require any annotation above and it is one way to run it , during my findings i came accross this post which helps in solving design issue which may occur if you go ahead with #InjectMocks approach)
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import com.comitemd.emr.datalayer.fhir.service.ObservationService;
import com.comitemd.emr.datalayer.fhir.utility.ResourceStringProvider;
import com.comitemd.emr.datalayer.fhir.utility.Utility;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.bson.Document;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Resource;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
//#RunWith(MockitoJUnitRunner.class)
public class ObservationControllerStandaloneTest {
private MockMvc mockMvc;
#Mock
private ObservationService observationService;
#InjectMocks
private ObservationController observationController;
private Document responseDocument;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);// enable this or MockitoJUnitRunner.class
Resource resource = Utility
.convertFromStringToFhirResource(Observation.class, ResourceStringProvider.observationResponse());
responseDocument=Utility.convertFromFhirResourceToMongoInsertibleDoc(resource);
this.mockMvc = MockMvcBuilders.standaloneSetup(observationController).build();
}
#Test
public void createObservationResource()throws Exception{
Mockito.when(observationService.createObservation(ResourceStringProvider.observationsource()))
.thenReturn(responseDocument);
MockHttpServletResponse response = mockMvc.perform(
post("/api/Observation")
.contentType(MediaType.APPLICATION_JSON)
.content(ResourceStringProvider.observationsource()))
.andDo(MockMvcResultHandlers.print())
.andReturn()
.getResponse();
verify(observationService, times(1)).createObservation(Mockito.any());
assertThat(response.getContentAsString()).isEqualTo(new ObjectMapper().writeValueAsString(responseDocument));
}
}
I am new to Spring MVC. I need to know how to consume RESTful api in UI.
And also I need to know can we use the api for the same app or we shall create new app to consume these api produced from REST. I have built a REST api in my project and I used the api in the same project with following code. But it didnt work.
RestClient.java
package com.spring.template;
import org.springframework.web.client.RestTemplate;
import com.spring.model.Employee;
public class RestClient {
public static void main(String[] args) {
try {
RestTemplate restTemplate = new RestTemplate();
final String base_url = "http://localhost:8080/SpringWebSevices/";
Employee employee = restTemplate.getForObject(base_url, Employee.class, 200);
System.out.println("Id:"+employee.getEmpid());
}
catch (Exception e) {
// TODO: handle exception
System.out.println(e.getMessage());
}
}
}
EmployeeController.java
package com.spring.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.spring.model.Employee;
import com.spring.service.EmployeeService;
#RestController
public class EmployeeController {
#Autowired
EmployeeService employeeService;
#RequestMapping(value ="/", method = RequestMethod.GET, produces ="application/json")
public ResponseEntity<List<Employee>> employees() {
HttpHeaders headers = new HttpHeaders();
List<Employee> employee = employeeService.getEmployees();
if(employee == null) {
return new ResponseEntity<List<Employee>>(HttpStatus.NOT_FOUND);
}
headers.add("Number of records found:", String.valueOf(employee.size()));
return new ResponseEntity<List<Employee>>(employee, HttpStatus.OK);
}
#RequestMapping(value="/employee/add", method = RequestMethod.POST , produces ="application/json")
public ResponseEntity<Employee> addEmployee(#RequestBody Employee employee) {
HttpHeaders headers = new HttpHeaders();
if(employee == null) {
return new ResponseEntity<Employee>(HttpStatus.BAD_REQUEST);
}
employeeService.createEmployee(employee);
headers.add("Added employee id:", String.valueOf(employee.getEmpid()));
return new ResponseEntity<Employee>(employee, headers, HttpStatus.CREATED);
}
#RequestMapping(value = "/employee/edit/{id}",method=RequestMethod.PUT)
public ResponseEntity<Employee> editEmployee(#PathVariable("id") int empid,#RequestBody Employee employee) {
HttpHeaders headers = new HttpHeaders();
Employee isExist = employeeService.getEmployee(empid);
if(isExist == null) {
return new ResponseEntity<Employee>(HttpStatus.NOT_FOUND);
} else if(employee == null) {
return new ResponseEntity<Employee>(HttpStatus.BAD_GATEWAY);
}
employeeService.updateEmployee(employee);
headers.add("Employee updated:", String.valueOf(employee.getEmpid()));
return new ResponseEntity<Employee>(employee,headers,HttpStatus.OK);
}
#RequestMapping(value = "/employee/delete/{id}", method =RequestMethod.DELETE)
public ResponseEntity<Employee> deleteEmployee(#PathVariable("id") int empid) {
HttpHeaders headers = new HttpHeaders();
Employee employee = employeeService.getEmployee(empid);
if(employee == null) {
return new ResponseEntity<Employee>(HttpStatus.NOT_FOUND);
}
employeeService.deleteEmployee(empid);
headers.add("Employee deleted:", String.valueOf(empid));
return new ResponseEntity<Employee>(employee, headers, HttpStatus.NO_CONTENT);
}
}
This is the error i got:
nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of com.spring.model.Employee out of START_ARRAY token
com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of com.spring.model.Employee out of START_ARRAY token
This looks like your sending a list of Employee to some method that accepts a single Employee as a parameter. Possibilities:
addEmployee
editEmployee
Your controller is returning List<Employee>, while you are trying to assign the result to a single Employee object in the line
Employee employee = restTemplate.getForObject(base_url, Employee.class, 200);
You are facing type incompatibilities, you could try
ResponseEntity<? extends ArrayList<Employee>> responseEntity = restTemplate.getForEntity(base_url, (Class<? extends ArrayList<Employee>)ArrayList.class, 200);
I haven't tested it yet but it should work.
I have the following controller:
RestApiController.java
package com.spring.ocr.rest.restconsume.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import com.spring.ocr.rest.restconsume.model.User;
import com.spring.ocr.rest.restconsume.service.UserService;
import com.spring.ocr.rest.restconsume.util.CustomErrorType;
#RestController
#RequestMapping("/api")
public class RestApiController {
#Autowired
UserService userService;
#RequestMapping(method = RequestMethod.GET, value = "/user/")
public ResponseEntity<List<User>> listAllUsers() {
final List<User> users = userService.findAllUsers();
if(users.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<List<User>>(users, HttpStatus.OK);
}
#RequestMapping(method = RequestMethod.GET, value = "/user/{id}")
public ResponseEntity<?> getUser(#PathVariable("id") long id) {
final User user = userService.findUserById(id);
if(user == null) {
return new ResponseEntity<>(new CustomErrorType(String.format("User with id %s not found", id)), HttpStatus.NOT_FOUND);
}
return new ResponseEntity<User>(user, HttpStatus.OK);
}
#RequestMapping(method = RequestMethod.POST, value = "/user/")
public ResponseEntity<?> createUser(#RequestBody User user, UriComponentsBuilder ucBuilder) {
if(userService.doesUserExist(user)) {
return new ResponseEntity<>(new CustomErrorType(String.format("Unable to create, user %s already exists", user.getName())), HttpStatus.CONFLICT);
}
userService.createUser(user);
final HttpHeaders headers = new HttpHeaders();
headers.setLocation(ucBuilder.path("/api/user/{id}").buildAndExpand(user.getId()).toUri());
return new ResponseEntity<String>(headers, HttpStatus.CREATED);
}
#RequestMapping(method = RequestMethod.PUT, value = "/user/{id}")
public ResponseEntity<?> updateUser(#PathVariable("id") long id,#RequestBody User user) {
User currentUser = userService.findUserById(id);
if(currentUser == null) {
return new ResponseEntity<>(new CustomErrorType(String.format("Unable to create update, User with id %s not found", user.getId())), HttpStatus.NOT_FOUND);
}
currentUser.setName(user.getName());
currentUser.setAge(user.getAge());
currentUser.setSalary(user.getSalary());
userService.updateUser(currentUser);
return new ResponseEntity<User>(currentUser, HttpStatus.OK);
}
#RequestMapping(method = RequestMethod.DELETE, value = "/user/{id}")
public ResponseEntity<?> deleteUser(#PathVariable("id") long id) {
User user = userService.findUserById(id);
if(user == null) {
return new ResponseEntity<>(new CustomErrorType(String.format("Unable to delete user, user with id %s not found", id)), HttpStatus.NOT_FOUND);
}
userService.deleteUserById(id);
return new ResponseEntity<User>(HttpStatus.NO_CONTENT);
}
#RequestMapping(method = RequestMethod.DELETE, value = "/user/")
public ResponseEntity<User> deleteAllUsers() {
userService.deleteAllUsers();
return new ResponseEntity<User>(HttpStatus.NO_CONTENT);
}
}
And I've set up a test of the web layer using mockMvc, with the user service bean mocked out as is standard:
RestApiControllerUnitTest.java
package com.spring.ocr.rest.restconsume.controller;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.ArrayList;
import java.util.List;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.ocr.rest.restconsume.model.User;
import com.spring.ocr.rest.restconsume.service.UserService;
#RunWith(SpringRunner.class)
#WebMvcTest(RestApiController.class)
public class RestApiControllerUnitTest {
#Autowired
private MockMvc mockMvc;
#MockBean
UserService userService;
#Autowired
ObjectMapper objectMapper;
private final List<User> dummyUserList = getDummyUserList();
private final User dummyUser = new User((long)1, "Dave", (short)30, (double)30000);
private final User dummyUpdatedUser = new User((long)1, "David", (short)31, (double)35000);
#Test
public void test_listAllUsers_userListSizeIs4_returnsListSizeOf4AndOkStatus() throws Exception {
when(userService.findAllUsers()).thenReturn(dummyUserList);
this.mockMvc.perform(get("/api/user/"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(4)));
}
#Test
public void test_listAllUsers_userListIsEmpty_returnsNoContentStatus() throws Exception {
when(userService.findAllUsers()).thenReturn(new ArrayList<User>());
this.mockMvc.perform(get("/api/user/"))
.andDo(print())
.andExpect(status().isNoContent());
}
#Test
public void test_getUser_userExists_returnsUser() throws Exception {
when(userService.findUserById(dummyUser.getId())).thenReturn(dummyUser);
this.mockMvc.perform(get("/api/user/" + dummyUser.getId()))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id",is((int)dummyUser.getId())))
.andExpect(jsonPath("$.name", is(dummyUser.getName())))
.andExpect(jsonPath("$.age", is((int)dummyUser.getAge())))
.andExpect(jsonPath("$.salary", is(dummyUser.getSalary())));
}
#Test
public void test_getUser_userDoesntExist_returnsNotFoundStatusAndCustomErrorString() throws Exception {
when(userService.findUserById(dummyUser.getId())).thenReturn(null);
this.mockMvc.perform(get("/api/user/"+dummyUser.getId()))
.andDo(print())
.andExpect(status().isNotFound())
.andExpect(content().string(containsString("User with id 1 not found")));
}
#Test
public void test_createUser_userDoesNotExist_userCreated() throws Exception {
final String dummyUserJson = objectMapper.writeValueAsString(dummyUser);
when(userService.doesUserExist(dummyUser)).thenReturn(false);
MvcResult result = this.mockMvc.perform(post("/api/user/")
.contentType(MediaType.APPLICATION_JSON)
.content(dummyUserJson))
.andDo(print())
.andExpect(status().isCreated())
.andReturn();
final String header = result.getResponse().getHeader("Location");
assertThat(header, is("http://localhost/api/user/1"));
}
#Test
public void test_createUser_userExists_returnsNotFoundStatusAndCustomErrorMessage() throws Exception {
final String dummyUserJson = objectMapper.writeValueAsString(dummyUser);
final String expectedContent = String.format("Unable to create, user %s already exists", dummyUser.getName());
when(userService.doesUserExist(anyObject())).thenReturn(true);
this.mockMvc.perform(post("/api/user/")
.contentType(MediaType.APPLICATION_JSON)
.content(dummyUserJson))
.andDo(print())
.andExpect(status().isConflict())
.andExpect(jsonPath("$.errorMessage", is(expectedContent)));
}
#Test
public void test_updateUser_userExists_returnsOkStatusAndUpdatedUser() throws Exception {
final String dummyUpdatedUserJson = objectMapper.writeValueAsString(dummyUpdatedUser);
when(userService.findUserById(dummyUser.getId())).thenReturn(dummyUser);
doNothing().when(userService).updateUser(dummyUser);
this.mockMvc.perform(put("api/user/" + dummyUser.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(dummyUpdatedUserJson))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.age", is(dummyUpdatedUser.getAge())))
.andExpect(jsonPath("$.name", is(dummyUpdatedUser.getName())))
.andExpect(jsonPath("$.salary", is(dummyUpdatedUser.getSalary())));
}
#Test
public void test_deleteUser_userExists_returnNoContentStatus() throws Exception {
when(userService.findUserById(dummyUser.getId())).thenReturn(dummyUser);
this.mockMvc.perform(delete("api/user/" + dummyUser.getId()))
.andDo(print())
.andExpect(status().isNotFound());
}
#Test
public void test_deleteUser_userDoesntExist_returnsNotFoundStatusAndCustomErrorMessage () throws Exception {
when(userService.findUserById(dummyUser.getId())).thenReturn(null);
final String expectedContent = String.format("Unable to create update, User with id %s not found", dummyUser.getName());
this.mockMvc.perform(delete("api/user/"+dummyUser.getId()))
.andDo(print())
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.errorMessage", is(expectedContent)));
}
#Test
public void test_deleteAllUsers_usersListPopulated_returnNoContentStatus() throws Exception {
this.mockMvc.perform(delete("api/user/").contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isNotFound());
}
private List<User> getDummyUserList() {
final List<User> dummyUserList = new ArrayList<>();
dummyUserList.add(new User((long)1, "Dave", (short)30, (double)30000));
dummyUserList.add(new User((long)2, "Jess", (short)20, (double)20000));
dummyUserList.add(new User((long)3, "Mike", (short)40, (double)40000));
dummyUserList.add(new User((long)4, "Molly", (short)50, (double)50000));
return dummyUserList;
}
}
The test test_updateUser_userExists_returnsOkStatusAndUpdatedUser is returning a 404 rather than a 200 status and test_deleteUser_userDoesntExist_returnsNotFoundStatusAndCustomErrorMessage is not returning the error message in the body, which alludes to the 404 not being a "genuine" 404(its not returned because the correct response is coming back, its returning it for some other reason). Im also thinking that some of the other 404 statuses may be returned under the same context.
Issue was due to missing "/" on the controller methods used in the failing tests.
I have the following request handler for saving autos. I have verified that this works when I use e.g. cURL. Now I want to unit test the method with Spring MVC Test. I have tried to use the fileUploader, but I am not managing to get it working. Nor do I manage to add the JSON part.
How would I unit test this method with Spring MVC Test? I am not able to find any examples on this.
#RequestMapping(value = "autos", method = RequestMethod.POST)
public ResponseEntity saveAuto(
#RequestPart(value = "data") autoResource,
#RequestParam(value = "files[]", required = false) List<MultipartFile> files) {
// ...
}
I want to uplod a JSON representation for my auto + one or more files.
I will add 100 in bounty to the correct answer!
Since MockMvcRequestBuilders#fileUpload is deprecated, you'll want to use MockMvcRequestBuilders#multipart(String, Object...) which returns a MockMultipartHttpServletRequestBuilder. Then chain a bunch of file(MockMultipartFile) calls.
Here's a working example. Given a #Controller
#Controller
public class NewController {
#RequestMapping(value = "/upload", method = RequestMethod.POST)
#ResponseBody
public String saveAuto(
#RequestPart(value = "json") JsonPojo pojo,
#RequestParam(value = "some-random") String random,
#RequestParam(value = "data", required = false) List<MultipartFile> files) {
System.out.println(random);
System.out.println(pojo.getJson());
for (MultipartFile file : files) {
System.out.println(file.getOriginalFilename());
}
return "success";
}
static class JsonPojo {
private String json;
public String getJson() {
return json;
}
public void setJson(String json) {
this.json = json;
}
}
}
and a unit test
#WebAppConfiguration
#ContextConfiguration(classes = WebConfig.class)
#RunWith(SpringJUnit4ClassRunner.class)
public class Example {
#Autowired
private WebApplicationContext webApplicationContext;
#Test
public void test() throws Exception {
MockMultipartFile firstFile = new MockMultipartFile("data", "filename.txt", "text/plain", "some xml".getBytes());
MockMultipartFile secondFile = new MockMultipartFile("data", "other-file-name.data", "text/plain", "some other type".getBytes());
MockMultipartFile jsonFile = new MockMultipartFile("json", "", "application/json", "{\"json\": \"someValue\"}".getBytes());
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
mockMvc.perform(MockMvcRequestBuilders.multipart("/upload")
.file(firstFile)
.file(secondFile)
.file(jsonFile)
.param("some-random", "4"))
.andExpect(status().is(200))
.andExpect(content().string("success"));
}
}
And the #Configuration class
#Configuration
#ComponentScan({ "test.controllers" })
#EnableWebMvc
public class WebConfig extends WebMvcConfigurationSupport {
#Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
return multipartResolver;
}
}
The test should pass and give you output of
4 // from param
someValue // from json file
filename.txt // from first file
other-file-name.data // from second file
The thing to note is that you are sending the JSON just like any other multipart file, except with a different content type.
The method MockMvcRequestBuilders.fileUpload is deprecated use MockMvcRequestBuilders.multipart instead.
This is an example:
import static org.hamcrest.CoreMatchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.multipart.MultipartFile;
/**
* Unit test New Controller.
*
*/
#RunWith(SpringRunner.class)
#WebMvcTest(NewController.class)
public class NewControllerTest {
private MockMvc mockMvc;
#Autowired
WebApplicationContext wContext;
#MockBean
private NewController newController;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wContext)
.alwaysDo(MockMvcResultHandlers.print())
.build();
}
#Test
public void test() throws Exception {
// Mock Request
MockMultipartFile jsonFile = new MockMultipartFile("test.json", "", "application/json", "{\"key1\": \"value1\"}".getBytes());
// Mock Response
NewControllerResponseDto response = new NewControllerDto();
Mockito.when(newController.postV1(Mockito.any(Integer.class), Mockito.any(MultipartFile.class))).thenReturn(response);
mockMvc.perform(MockMvcRequestBuilders.multipart("/fileUpload")
.file("file", jsonFile.getBytes())
.characterEncoding("UTF-8"))
.andExpect(status().isOk());
}
}
Have a look at this example taken from the spring MVC showcase, this is the link to the source code:
#RunWith(SpringJUnit4ClassRunner.class)
public class FileUploadControllerTests extends AbstractContextControllerTests {
#Test
public void readString() throws Exception {
MockMultipartFile file = new MockMultipartFile("file", "orig", null, "bar".getBytes());
webAppContextSetup(this.wac).build()
.perform(fileUpload("/fileupload").file(file))
.andExpect(model().attribute("message", "File 'orig' uploaded successfully"));
}
}
Here's what worked for me, here I'm attaching a file to my EmailController under test. Also take a look at the postman screenshot on how I'm posting the data.
#WebAppConfiguration
#RunWith(SpringRunner.class)
#SpringBootTest(
classes = EmailControllerBootApplication.class
)
public class SendEmailTest {
#Autowired
private WebApplicationContext webApplicationContext;
#Test
public void testSend() throws Exception{
String jsonStr = "{\"to\": [\"email.address#domain.com\"],\"subject\": "
+ "\"CDM - Spring Boot email service with attachment\","
+ "\"body\": \"Email body will contain test results, with screenshot\"}";
Resource fileResource = new ClassPathResource(
"screen-shots/HomePage-attachment.png");
assertNotNull(fileResource);
MockMultipartFile firstFile = new MockMultipartFile(
"attachments",fileResource.getFilename(),
MediaType.MULTIPART_FORM_DATA_VALUE,
fileResource.getInputStream());
assertNotNull(firstFile);
MockMvc mockMvc = MockMvcBuilders.
webAppContextSetup(webApplicationContext).build();
mockMvc.perform(MockMvcRequestBuilders
.multipart("/api/v1/email/send")
.file(firstFile)
.param("data", jsonStr))
.andExpect(status().is(200));
}
}
If you are using Spring4/SpringBoot 1.x, then it's worth mentioning that you can add "text" (json) parts as well . This can be done via MockMvcRequestBuilders.fileUpload().file(MockMultipartFile file) (which is needed as method .multipart() is not available in this version):
#Test
public void test() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders.fileUpload("/files")
// file-part
.file(makeMultipartFile( "file-part" "some/path/to/file.bin", "application/octet-stream"))
// text part
.file(makeMultipartTextPart("json-part", "{ \"foo\" : \"bar\" }", "application/json"))
.andExpect(status().isOk())));
}
private MockMultipartFile(String requestPartName, String filename,
String contentType, String pathOnClassPath) {
return new MockMultipartFile(requestPartName, filename,
contentType, readResourceFile(pathOnClasspath);
}
// make text-part using MockMultipartFile
private MockMultipartFile makeMultipartTextPart(String requestPartName,
String value, String contentType) throws Exception {
return new MockMultipartFile(requestPartName, "", contentType,
value.getBytes(Charset.forName("UTF-8")));
}
private byte[] readResourceFile(String pathOnClassPath) throws Exception {
return Files.readAllBytes(Paths.get(Thread.currentThread().getContextClassLoader()
.getResource(pathOnClassPath).toUri()));
}
}
I have POST method in a Spring boot rest controller as follows
#RequestMapping(value="/post/action/bookmark", method=RequestMethod.POST)
public #ResponseBody Map<String, String> bookmarkPost(
#RequestParam(value="actionType",required=true) String actionType,
#RequestParam(value="postId",required=true) String postId,
#CurrentUser User user) throws Exception{
return service.bookmarkPost(postId, actionType, user);
}
now if I test with missing parameter in Postman I get an 400 http response and a JSON body:
{
"timestamp": "2015-07-20",
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.MissingServletRequestParameterException",
"message": "Required String parameter 'actionType' is not present",
"path": "/post/action/bookmark"
}
until now it's OK, but when I try to unit test I don't get the JSON response back
#Test
public void bookmarkMissingActionTypeParam() throws Exception{
// #formatter:off
mockMvc.perform(
post("/post/action/bookmark")
.accept(MediaType.APPLICATION_JSON)
.param("postId", "55ab8831036437e96e8250b6")
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.exception", containsString("MissingServletRequestParameterException")));
// #formatter:on
}
the test fails and produces
java.lang.IllegalArgumentException: json can not be null or empty
I did a .andDo(print()) and found that there is no body in the response
MockHttpServletResponse:
Status = 400
Error message = Required String parameter 'actionType' is not present
Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store], Pragma=[no-cache], Expires=[1], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
why am I not getting the JSON response while unit testing my controller, but do receive it in manual testing using Postman or cUrl?
EDIT: I've added #WebIntegrationTest but got the same error:
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.http.MediaType;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = RestApplication.class)
#WebIntegrationTest
public class PostControllerTest {
private MockMvc mockMvc;
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private FilterChainProxy springSecurityFilterChain;
#Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.addFilter(springSecurityFilterChain)
.build();
}
#Test
public void bookmarkMissingActionTypeParam() throws Exception{
// #formatter:off
mockMvc.perform(
post("/post/action/bookmark")
.accept(MediaType.APPLICATION_JSON)
.param("postId", "55ab8831036437e96e8250b6")
)
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.exception", containsString("MissingServletRequestParameterException")));
// #formatter:on
}
}
This is because Spring Boot has auto-configured an exception handler org.springframework.boot.autoconfigure.web.BasicErrorController which is probably not present in your unit tests. A way to get it will be to use the Spring Boot testing support related annotations:
#SpringApplicationConfiguration
#WebIntegrationTest
More details are here
Update:
You are absolutely right, the behavior is very different in UI vs in test, the error pages which respond to status codes are not correctly hooked up in a non-servlet test environment. Improving this behavior can be a good bug to open for Spring MVC and/or Spring Boot.
For now, I have a workaround which simulates the behavior of BasicErrorController the following way:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = {RestApplication.class, TestConfiguration.class})
#WebIntegrationTest
public class PostControllerTest {
private MockMvc mockMvc;
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private FilterChainProxy springSecurityFilterChain;
#Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.addFilter(springSecurityFilterChain)
.build();
}
#Test
public void bookmarkMissingActionTypeParam() throws Exception{
// #formatter:off
mockMvc.perform(
post("/post/action/bookmark")
.accept(MediaType.APPLICATION_JSON)
.param("postId", "55ab8831036437e96e8250b6")
)
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.exception", containsString("MissingServletRequestParameterException")));
// #formatter:on
}
}
#Configuration
public static class TestConfiguration {
#Bean
public ErrorController errorController(ErrorAttributes errorAttributes) {
return new ErrorController(errorAttributes);
}
}
#ControllerAdvice
class ErrorController extends BasicErrorController {
public ErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes);
}
#Override
#ExceptionHandler(Exception.class)
#ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
return super.error(request);
}
}
What I am doing here is adding a ControllerAdvice which handles the Exception flow and delegates back to the BasicErrorController. This would atleast make the behavior consistent for you.
Originally, it should fix the error by #ResponseBody tag when defining your REST controller method. it will fix json error in the test class.
But, as you are using spring boot, you will define the controller class with #RestController and it should automatically take care of the error without defining #Controller and #ResponseType tags.