#PostMapping("/addCompany")
public Company createCompany(#Valid #RequestBody Company company, Errors errors) {
if (errors.hasErrors()) {
throw new ValidationException(errors.getFieldError().getDefaultMessage());
}
return companyService.addCompany(company);
}
You can test it in two ways:
unit test of controller
web layer test
Unit test:
Mock Errors and call createCompany with that mock as an argument.
Verify that exception is thrown
Web layer test:
post an invalid request with mockMvc
check that error code is returned
#WebMvcTest(CompanyController.class)
class CompanyControllerTest {
private static final String CREATE_COMPANY_ENDPOINT = "/addCompany/";
#Autowired
private MockMvc mockMvc;
#Test
void respondsWith4xxOnInvalidCreateCompanyRequest() throws Exception {
String invalidRequest = "{\"id\":\"aaa\", \"name\":\"bbb\"}"
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders
.post(CREATE_COMPANY_ENDPOINT)
.content(invalidRequest)
.contentType(MediaType.APPLICATION_JSON_VALUE);
this.mockMvc.perform(builder)
.andExpect(status().is4xxClientError());
}
}
In this case, I have a strong preference to web layer test - you check actual validation logic. Mocking Errors buys you nothing except code coverage.
On top of that:
Your handling of validation errors is trivial. If you drop Errors from the parameter list, on invalid input Spring will throw MethodArgumentNotValidException with BindingResult, which has all the data you need.
Related
I'm working at a small parking service REST API.
For example I have endpoint:
#RequestMapping("/departure")
public ResponseEntity<String> departure(#RequestBody CarAtGateModel carAtGateModel) throws Exception {
return parkingService.carDeparture(carAtGateModel.getCarEntity().getIdCar());
}
method parkingService.carDeparture looks like this:
public ResponseEntity<String> carDeparture(String carID) throws UnidentifiedCarException {
CarAndParkingIDsEntity carAndParkingIDsEntity = carAndParkingIDsRepository.findByIdCar(carID);
if (carAndParkingIDsEntity == null) {
throw new UnidentifiedCarException();
} else {
carAndParkingIDsEntity.setIdParking("-1");
carAndParkingIDsRepository.flush();
return new ResponseEntity<>("Gate up", HttpStatus.OK);
}
}
And the problem is when I'm trying to do some integration tests. Unit tests for parking service pass correctly but I don't know exactly what should I do for integration tests.
I was thinking about something like. Mock carAtGateModel (it's carId and ParkingId) and send it to the endpoint and then mock parkingservice because I'm using it inside and I don't want to change data in the database.
when(parkingService.carDeparture(anyString())).thenReturn(new ResponseEntity<>("Gate up",
HttpStatus.OK));
HttpEntity<CarAtGateModel> request = new HttpEntity<>(carAtGateModel);
ResponseEntity<String> response = testRestTemplate.postForEntity("/departure", request,
String.class);
I would suggest you use #WebMvcTest instead to have a slice test (sort of integration test that focuses on a single application layer).
#ExtendWith(SpringExtension.class)
#WebMvcTest(YourController.class)
public class YourControllerIntegrationTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private ParkingService parkingService;
// Your test cases here
}
You would use mockMvc to perform an actual call to the REST endpoint with an actual CarAtGateModel object as JSON in the request body. You would also mock the behaviour of your ParkingService, thus focusing the test on your Controller.
I am pretty new to spring controller. I am trying to write unit test for invalid parameter. I have an api that has #RequestParam("id") #Min(1) long id and in my unit test, I pass in "-1". Here is my test:
#Test
public void searchWithInvalidIbId() throws Exception {
mockMvc.perform(get(BASE_URL)
.param(COLUMN_IB_ID, INVALID_IB_ID_VALUE) // = "-1"
.param(COLUMN_TIME_RANGE, TIME_RANGE_VALUE)
.param(COLUMN_TIME_ZONE, TIME_ZONE_VALUE)
.accept(PowerShareMediaType.PSH_DISPATCH_REPORTER_V1_JSON)
.contentType(PowerShareMediaType.PSH_DISPATCH_REPORTER_V1_JSON))
.andExpect(status().isBadRequest());
}
When I run this, I get
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is javax.validation.ConstraintViolationException: search.arg2: must be greater than or equal to 1
It makes sense, but I am not sure how to test this is BadRequest. I tried #Test(expected = NestedServletException.class), and it passed, but I don't think it is checking what I want to check. What is the right approach to check this?
You can have your custom exception handler annotated with #ControllerAdvice and handle ConstraintViolationException in that class. You can throw your custom exception with additional details if you wish.
Here is an example approach:
#ControllerAdvice
public class MyCustomExceptionHandler {
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(ConstraintViolationException.class)
ApiError constraintViolationException(ConstraintViolationException e) {
return BAD_REQUEST.apply(e.getBindingResult());
}
}
Here ApiError is a custom class to represent your error response, it can be anything else you want. You can add timestamp, http status, your error message etc.
I would like the application to return JSON object from my Java Classes (both success and fail cases).
I had defined a #RestControllerAdvice to handle the errors from controller. My program also shows the error message correctly in json, but the problem is in the unit test.
The problem is when it throws:
org.springframework.web.bind.MethodArgumentNotValidException
My unit test failed with error :
java.lang.AssertionError: Response header 'content-type' expected:<application/json;charset=UTF-8> but was:<null>
Controller:
#PostMapping("/import")
public ResponseEntity<StatusModel> import(#Valid #RequestBody ImportModel importModel ){
//logic
return new ResponseEntity<>(new StatusModel("Data accepted."), HttpStatus.OK);
}
Unit Test:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {MockConfiguration.class})
#WebAppConfiguration
public class ModelControllerTest {
private MockMvc mockMvc;
#InjectMocks
private ModelController controller;
#Before
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
#Test
public void import_validRequest_imported() throws Exception {
mockMvc
.perform(
post("/import")
.content(VALID_CONTENT).contentType("application/json;charset=UTF-8"))
.andExpect(status().isOk())
.andExpect(header().string("content-type", "application/json;charset=UTF-8"))
.andExpect(jsonPath("$.status", equalTo("Data accepted")));
}
#Test
public void import_invalidRequest_notImported() throws Exception {
mockMvc
.perform(
post("/import")
.content(INVALID_CONTENT).contentType("application/json"))
.andExpect(status().isBadRequest())
.andDo(print())
.andExpect(header().string("content-type", "application/json")); <----- This assertion failed
}
}
MockHttpServletRequest log:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /import
Parameters = {}
Headers = {Content-Type=[application/json]}
Handler:
Type = com.test.ModelController
Method = public org.springframework.http.ResponseEntity<com.model.StatusModel> com.ModelController.import(com.test.model.ImportModel)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = org.springframework.web.bind.MethodArgumentNotValidException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 400
Error message = null
Headers = {}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Why is the content type, error message is empty?
Here is the rational why mock mvc doesn't support spring boot exception handlers followed by recommendation and fix.
Rational excerpt
Spring Boot's error handling is based on Servlet container error
mappings that result in an ERROR dispatch to an ErrorController.
MockMvc however is container-less testing so with no Servlet container
the exception simply bubbles up with nothing to stop it.
So MockMvc tests simply aren't enough to test error responses
generated through Spring Boot. I would argue that you shouldn't be
testing Spring Boot's error handling. If you're customizing it in any
way you can write Spring Boot integration tests (with an actual
container) to verify error responses. And then for MockMvc tests focus
on fully testing the web layer while expecting exceptions to bubble
up.
This is a typical unit vs integration tests trade off. You do unit
tests even if they don't test everything because they give you more
control and run faster.
Recommendation excerpt
How can we write tests for error conditions using default spring-boot
JSON responses, then?
#xak2000 Rossen's already covered this, but I wanted to give you a
direct answer. If you really want to test the precise format of the
error response then you can use an integration test using
#SpringBootTest configured with a DEFINED_PORT or RANDOM_PORT web
environment and TestRestTemplate.
Complete details here
https://github.com/spring-projects/spring-boot/issues/7321
Fix
Here is slightly different error validation using Spring Boot test.
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
#SpringBootTest(classes = DemoApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ModelControllerTest {
#Autowired
private TestRestTemplate restTemplate;
#Test
void import_invalidRequest_notImported() throws JSONException {
String expected = "{\"status\":400,\"error\":\"Bad Request\",\"message\":\"JSON parse error: Unrecognized token 'Invalid': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'Invalid': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\\n at [Source: (PushbackInputStream); line: 1, column: 8]\",\"path\":\"/import\"}";
String invalidJson = "Invalid";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(invalidJson, headers);
ResponseEntity<String> response = restTemplate.exchange("/import", HttpMethod.POST, entity, String.class);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType());
JSONAssert.assertEquals(expected, response.getBody(), false);
}
}
Reference here
https://mkyong.com/spring-boot/spring-rest-integration-test-example/
due to this tutorial - https://www.baeldung.com/spring-boot-custom-error-page I wanted to customize my error page ie. when someone go to www.myweb.com/blablablalb3 I want to return page with text "wrong url request".
All works fine:
#Controller
public class ApiServerErrorController implements ErrorController {
#Override
public String getErrorPath() {
return "error";
}
#RequestMapping("/error")
public String handleError() {
return "forward:/error-page.html";
}
}
But I dont know how to test it:
#Test
public void makeRandomRequest__shouldReturnErrorPage() throws Exception {
this.mockMvc.perform(get(RANDOM_URL))
.andDo(print());
}
print() returns:
MockHttpServletResponse:
Status = 404
Error message = null
Headers = {X-Application-Context=[application:integration:-1]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
So I cant created something like this:
.andExpect(forwardedUrl("error-page"));
because it fails, but on manual tests error-page is returned.
Testing of a custom ErrorController with MockMvc is unfortunately not supported.
For a detailed explanation, see the official recommendation from the Spring Boot team (source).
To be sure that any error handling is working fully, it's necessary to
involve the servlet container in that testing as it's responsible for
error page registration etc. Even if MockMvc itself or a Boot
enhancement to MockMvc allowed forwarding to an error page, you'd be
testing the testing infrastructure not the real-world scenario that
you're actually interested in.
Our recommendation for tests that want to be sure that error handling
is working correctly, is to use an embedded container and test with
WebTestClient, RestAssured, or TestRestTemplate.
My suggestion is to use #ControllerAdvice
In this way you can work around the problem and you can continue to use MockMvc with the big advantage that you are not required to have a running server.
Of course to test explicitly the error page management you need a running server. My suggestion is mainly for those who implemented ErrorController but still want to use MockMvc for unit testing.
#ControllerAdvice
public class MyControllerAdvice {
#ExceptionHandler(FileSizeLimitExceededException.class)
public ResponseEntity<Throwable> handleFileException(HttpServletRequest request, FileSizeLimitExceededException ex) {
return new ResponseEntity<>(ex, HttpStatus.PAYLOAD_TOO_LARGE);
}
#ExceptionHandler(Throwable.class)
public ResponseEntity<Throwable> handleUnexpected(HttpServletRequest request, Throwable throwable) {
return new ResponseEntity<>(throwable, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
i created post method in mockMVC (in spring boot project)
This is my method testing
This is my method testing
#Test
public void createAccount() throws Exception {
AccountDTO accountDTO = new AccountDTO("SAVINGS", "SAVINGS");
when(addaccountService.findByName("SAVING")).thenReturn(Optional.empty());
when(addaccountService.createAccount(any())).thenReturn(createdAccountDTO);
CreatedAccountDTO createdAccountDTO = new CreatedAccountDTO("a#wp.pl", "SAVINGS", "1234rds", uuid);
mockMvc.perform(
post("/account").contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(AccountNewDTO)))
.andExpect(status().isCreated())
.andExpect(header().string("location", containsString("/account/"+uuid.toString())));
System.out.println("aaa");
}
I want to write GET method.
how to write a get method in mock mvc? how to verify whether what I threw was returned?
You can try the below for Mockmvc perform get and post methods
For get method
#Autowired
private MuffinRepository muffinRepository;
#Test
public void testgetMethod throws Exception(){
Muffin muffin = new Muffin("Butterscotch");
muffin.setId(1L);
BddMockito.given(muffinRepository.findOne(1L)).
willReturn(muffin);
mockMvc.perform(MockMvcRequestBuilders.
get("/muffins/1")).
andExpect(MockMvcResutMatchers.status().isOk()).
andExpect(MockMvcResutMatchers.content().string("{\"id\":1, "flavor":"Butterscotch"}"));
}
//Test to do post operation
#Test
public void testgetMethod throws Exception(){
Muffin muffin = new Muffin("Butterscotch");
muffin.setId(1L);
BddMockito.given(muffinRepository.findOne(1L)).
willReturn(muffin);
mockMvc.perform(MockMvcRequestBuilders.
post("/muffins")
.content(convertObjectToJsonString(muffin))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResutMatchers.status().isCreated())
.andExpect(MockMvcResutMatchers.content().json(convertObjectToJsonString(muffin)));
}
If the response is empty then make sure to override equals() and hashCode() method on the Entity your repository is working with
//Converts Object to Json String
private String convertObjectToJsonString(Muffin muffin) throws JsonProcessingException{
ObjectWriter writer = new ObjectWriter().writer().withDefaultPrettyPrinter();
return writer.writeValueAsString(muffin);
}
You can use the static get method of the class MockMvcRequestBuilders, see: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.html#get-java.lang.String-java.lang.Object...-
Example:
mockMvc.perform(get("/account")).andExpect(...);
If you throw an exception within your controller method it will typically trigger execution of an exception handler which transforms the exception into a HTTP error response. By default, you could check if the status of the response was 500. If you have implemented your own exception handler you may want to check the response body as well to verify if it contains the expected error data.