I'm using spring-boot-test with MockMvcRequestBuilders to test some GET rest webservice.
Question: is it possible to automatically translate a bean to a get-query?
Example:
#AutoConfigureMockMvc
public class WebTest {
#Autowired
protected MockMvc mvc;
#Test
public void test() {
MyRequest req = new MyRequest();
req.setFirstname("john");
req.setLastname("doe");
req.setAge(30);
mvc.perform(MockMvcRequestBuilders
.get(path)
.contentType(MediaType.APPLICATION_JSON)
.param(...) //TODO how to automatically add all params?
.andExpect(status().isOk());
}
}
public class MyRequest {
private String firstname;
private String lastname;
private int age;
}
I would need an auto translation to: ?firstname=john&lastname=doe&age=30, but in a more generic way not having to type the parameters statically.
I don't think there's anything available out-of-the-box for this specific requirement, but you can piece it together using a BeanWrapperImpl to access the properties from MyRequest and turn each into a call to param on the request builder:
MyRequest req = new MyRequest();
req.setFirstname("john");
req.setLastname("doe");
req.setAge(30);
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.get(path).contentType(MediaType.APPLICATION_JSON);
for (PropertyDescriptor property : new BeanWrapperImpl(req).getPropertyDescriptors()) {
if (property.getWriteMethod() != null) {
requestBuilder.param(property.getName(),
property.getReadMethod().invoke(req).toString());
}
}
mvc.perform(requestBuilder).andExpect(status().isOk());
Related
I'm having problems mocking the response object of my Test Class when using Mockito. I'm trying to test an exception, for this I need one of the attributes of the Class that returns from the POST request. I've successfully mocked the RestTemplate but my when().thenReturn() is not returning anything and I'm getting a null pointer exception at the "if" validation. If anyone could help me on this problem I would be very grateful.
Here is my Service Class:
#Service
public class CaptchaValidatorServiceImpl implements CaptchaValidatorService{
private static final String GOOGLE_CAPTCHA_ENDPOINT = "someEndpoint";
private String stage;
private String captchaSecret;
private RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
#Override
public void checkToken(String token) throws Exception{
MultiValueMap<String,String> requestMap = new LinkedValueMap<>();
requestMap.add("secret", captchaSecret);
requestMap.add("response", token);
try{
CaptchaResponse response = restTemplate.postForObject(GOOGLE_CAPTCHA_ENDPOINT,
requestMap, CaptchaResponse.class);
if(!response.getSuccess()){
throw new InvalidCaptchaTokenException("Invalid Token");
}
} catch (ResourceAccessException e){
throw new CaptchaValidationNotPossible("No Response from Server");
}
}
private SimpleClientHttpRequestFactory getClientHttpRequestFactory(){
...
}
}
And here is my Test Class:
#SpringBootTest
public class CaptchaValidatorTest{
#Mock
private RestTemplate restTemplate;
#InjectMocks
#Spy
private CaptchaValidatorServiceImpl captchaValidatorService;
private CaptchaResponse captchaResponse = mock(CaptchaResponse.class);
#Test
public void shouldThrowInvalidTokenException() {
captchaResponse.setSuccess(false);
Mockito.when(restTemplate.postForObject(Mockito.anyString(),
ArgumentMatchers.any(Class.class), ArgumentMatchers.any(Class.class)))
.thenReturn(captchaResponse);
Exception exception = assertThrows(InvalidCaptchaTokenException.class, () ->
captchaValidatorService.checkToken("test"));
assertEquals("Invalid Token", exception.getMessage());
}
}
In my opinion it could be a problem with ArgumentMatchers.
Method postForObject require parameters as String, MultiValueMap(or parent) and Class, but you set in Mockito.when: anyString() (correct), any(Class.class) (but MultiValueMap is passed - probably incorrect) and any(Class.class) (correct).
Try use:
Mockito.when(restTemplate.postForObject(ArgumentMatchers.any(String.class),
ArgumentMatchers.any(MultiValueMap.class), ArgumentMatchers.any(Class.class)))
.thenReturn(captchaResponse);
EDIT:
It seems to me that the CaptchaResponse in the test is unnecessarily a mock:
private CaptchaResponse captchaResponse = mock(CaptchaResponse.class);
but if You want this in that way, I think u need to replace:
captchaResponse.setSuccess(false);
to something like:
Mockito.when(captchaResponse.getSuccess()).thenReturn(false);
I am new to Unit Testing. After referring google, I created a Test class to test my Controller as follows:
#RunWith(SpringRunner.class)
#WebMvcTest(PromoController.class)
public class PromoApplicationTests {
#Autowired
protected MockMvc mvc;
#MockBean PromoService promoService;
protected String mapToJson(Object obj) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(obj);
}
protected <T> T mapFromJson(String json, Class<T> clazz)
throws JsonParseException, JsonMappingException, IOException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, clazz);
}
#Test
public void applyPromotionTest_1() throws Exception {
String uri = "/classPath/methodPath";
List<Cart> cartLs = new ArrayList<Cart>();
// added few objects to the list
String inputJson = mapToJson(cartLs);
MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.post(uri)
.contentType(MediaType.APPLICATION_JSON).content(inputJson)).andReturn();
int status = mvcResult.getResponse().getStatus();
assertEquals(200, status);
String actual = mvcResult.getResponse().getContentAsString();
String expected = "{\"key1\":val1, \"key2\":\"val 2\"}";
assertEquals(expected, actual, true);
}
}
I have the following Controller and service Class :
#RequestMapping("/classPath")
#RestController
public class PromoController {
#Autowired
PromoService promoService;
#PostMapping("/methodPath")
public PromoResponse applyPromo(#RequestBody List<Cart> cartObj) {
PromoResponse p = promoService.myMethod(cartObj);
return p;
}
}
#Component
public class PromoServiceImpl implements PromoService{
#Override
public PromoResponse myMethod(List<Cart> cartList) {
// myCode
}
}
When I debugged my unit test, p object in the controller was null.
I am getting status as 200 but not the expected JSON response
What am I missing here?
While using #WebMvcTest spring boot will only initialize the web layer and will not load complete application context. And you need to use #MockBean to create and inject mock while using #WebMvcTest.
Which you have done
#MockBean
PromoService promoService;
Spring Boot instantiates only the web layer rather than the whole context
We use #MockBean to create and inject a mock for the GreetingService (if you do not do so, the application context cannot start), and we set its expectations using Mockito.
But the, since it is mock bean you are responsible to mock the myMethod() call
#Test
public void applyPromotionTest_1() throws Exception {
String uri = "/classPath/methodPath";
List<Cart> cartLs = new ArrayList<Cart>();
// added few objects to the list
// create PromoResponse object you like to return
When(promoService.myMethod(ArgumentsMatchesr.anyList())).thenReturn(/*PromoResponse object */);
String inputJson = mapToJson(cartLs);
MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.post(uri)
.contentType(MediaType.APPLICATION_JSON).content(inputJson)).andReturn();
int status = mvcResult.getResponse().getStatus();
assertEquals(200, status);
String actual = mvcResult.getResponse().getContentAsString();
String expected = "{\"key1\":val1, \"key2\":\"val 2\"}";
assertEquals(expected, actual, true);
}
Background
The problem came up on my day job while implementing a POST multipart/form-data endpoint for a file upload with some meta information. I am not an expert in the Spring Boot ecosystem; it is likely that the problem is solved by a simple fix and that I am just missing the right term to search for.
Problem statement
To implement an endpoint for a file-upload with additional meta information, I wrote the following #RestController:
#RestController
#RequestMapping(Resource.ROOT)
#AllArgsConstructor(onConstructor = #__({#Inject}))
public class Resource {
public static final String ROOT = "/test";
private final Logger logger;
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.APPLICATION_JSON)
public ResponseEntity<Void> test(#Valid final Request request) {
logger.info("request = {}", request);
return ResponseEntity.ok().build();
}
}
With Request being specified as:
#Value
#AllArgsConstructor
public class Request {
#NotNull
String name;
#NotNull
MultipartFile file;
}
And a small happy path test:
#SpringBootTest
#AutoConfigureMockMvc
class TestCase {
#Autowired
private MockMvc mockMvc;
#Test
void shouldReturnOk() throws Exception {
// GIVEN
final byte[] content = Files.readAllBytes(Path.of(".", "src/test/resources/PenPen.png"));
final String name = "name";
// WHEN
// #formatter:off
mockMvc.perform(MockMvcRequestBuilders
.multipart(Resource.ROOT)
.file("file", content)
.param("name", name))
// THEN
.andExpect(status().isOk());
// #formatter:on
}
}
A complete MRE can be found on Bitbucket, branch problem-with-immutable-request.
When running the test (./mvnw test), it fails with the endpoint returning a 400 BAD REQUEST instead of 200 OK. Reading the logs reveals that request parameter file is null:
...
Content type = text/plain;charset=UTF-8
Body = file: must not be null.
...
I partially understand why it is null. With this partial knowledge, I was able to circumvent the problem by making the field file in Request mutable:
#ToString
#Getter
#AllArgsConstructor
public class Request {
#NotNull
private final String name;
#Setter
#NotNull
private MultipartFile file;
}
The code "fixing" the problem can be found on Bitbucket, branch problem-solved-by-making-field-mutable.
This, however, makes the Request mutable, which I would like to prevent. To further investigate, I unrolled the lombok annotations on Request and added some logging:
public class Request {
private static final Logger LOGGER = LoggerFactory.getLogger(Request.class);
#NotNull
private final String name;
#NotNull
private MultipartFile file;
public Request(final String name, final MultipartFile file) {
this.name = name;
this.setFile(file);
}
public #NotNull String getName() {
return this.name;
}
public #NotNull MultipartFile getFile() {
return this.file;
}
public String toString() {
return "Request(name=" + this.getName() + ", file=" + this.getFile() + ")";
}
public void setFile(final MultipartFile file) {
LOGGER.info("file = {}", file);
this.file = file;
}
}
Code of unrolled version can be found on Bitbucket, branch lombok-unrolled-for-debugging.
When looking at the log statements of the now successful test, we can see that Request::setFile is called twice:
2020-09-05 09:42:31.049 INFO 11012 --- [ main] d.turing85.springboot.multipart.Request : file = null
2020-09-05 09:42:31.056 INFO 11012 --- [ main] d.turing85.springboot.multipart.Request : file = org.springframework.mock.web
The first call comes from the constructor invocation. The second call, I imagine, comes from somewhere within Spring's mapping mechanism for the form parameters.
I know that there is the possibility to define the form parameters individually on the endpoint and constructing the Request instance within the method:
public class Resource {
...
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.APPLICATION_JSON)
public ResponseEntity<Void> test(
#RequestPart(name = "name") final String name,
#RequestPart(name = "file") final MultipartFile file) {
final Request request = new Request(name, file);
logger.info("request = {}", request);
return ResponseEntity.ok().build();
}
}
This will, however, result in other problems. For example, we would have to add an additional exception mapper for MissingServletRequestPartException and align the returned HTTP response with the existing response for BindException. I would like to avoid this if possible.
A search on the topic turned up Spring Boot controller - Upload Multipart and JSON to DTO. The solution, however, did not work for me since I do not use MVC (I think).
Question
Is there a possibility to keep Request immutable such that Spring is able to pass the MultipartFile to the all args constructor instead of setting it through the setter afterwards? Writing a custom mapper/converter is acceptable, but I did not find a possibility to write a mapper for either a specific endpoint or a specific type.
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> test(#Valid #ModelAttribute final RequestDto request) {
return ResponseEntity.ok().build();
}
It is still working with rest api call. But i really do not get immutability concern of yours.
If you define setter the multipart data you can use ModelAttribute.
#SpringBootTest
#AutoConfigureMockMvc
class FileUploadControllerIT {
#Autowired
private MockMvc mockMvc;
#Test
void shouldReturnOk() throws Exception {
// GIVEN
final byte[] content = Files.readAllBytes(Paths.get(Thread.currentThread().getContextClassLoader().getResource("text.txt").toURI()));
final String name = "name";
// WHEN
// #formatter:off
mockMvc.perform(MockMvcRequestBuilders
.multipart("/context/api/v1")
.file("multipartFile", content)
.param("name", name))
// THEN
.andExpect(status().isOk());
// #formatter:on
}
}
The above code works with ModelAttribute.
Also you are giving absolute path, i guess it is wrong. You can get file with classloader.
I have a rest controller with one method. This method takes one String argument annotated as #RequestBody. For some reason not mentioned here, I'm forced to use type String and manually convert it to TestDTO. From the API's consumer point of view body is type of TestDTO and I want to show this type in SwaggerUI.
Unfortunately (which is quite obvious) swagger shows that body is type of String. Look at the picture below.
What I want to achieve is to have String body in java code and TestDTO in swagger code. How can I force Swagger to show it? I tried to find annotations and its properties, but failed.
Rest controller code below:
#RestController
#Api(tags = { "test" }, description = "test related resources")
public class TestController {
#Autowired
ObjectMapper mapper;
#RequestMapping(path = "/test", method = RequestMethod.POST)
public void confirm(#RequestBody String requestBody) throws IOException {
//do sth with body
TestDTO dto = mapper.readValue(requestBody, TestDTO.class);
//do sth with dto
}
}
class TestDTO{
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
I figured it out. Two changes need to be made.
First, like in #Dave Pateral's answer #ApiImplicitParams must be added
#RestController
#Api(tags = { "test" }, description = "test related resources")
public class TestController {
#Autowired
ObjectMapper mapper;
#ApiImplicitParams({
#ApiImplicitParam(name = "requestBody", required = true,
dataType = "TestDTO", paramType = "body")
})
#RequestMapping(path = "/test", method = RequestMethod.POST)
public void confirm(#RequestBody String requestBody) throws IOException {
//do sth with body
TestDTO dto = mapper.readValue(requestBody, TestDTO.class);
//do sth with dto
}
}
And then implicit Model must be registered in the docket, minimal working example below
#Configuration
public class SwaggerConfiguration {
#Autowired
private TypeResolver typeResolver;
#Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.additionalModels(typeResolver.resolve(TestDTO.class));
}
}
And the result is
Try put this annotation on your method:
#ApiImplicitParam(name = "test", value = "testDTO", required = true, dataType = "TestDTO")
I have a resource for rest API which uses a service.
This service has a constructor with parameters.
I want to test this resource and to mock this service.
This Question: How to pass parameters to REST resource using Jersey 2.5
wasn't helpful because they used #Inject and I cannot use it.
Any suggestions?
The second question is how do I pass parameter to test this resouce:
My code is:
#Path("/2/{subversion: [0-3]}/users")
public class UserResource {
Logger log = Logger.getLogger(UserResource.class);
private MyService service;
public void setService(Service ser) {
this.service = ser;
}
#Context HttpServletRequest currentRequest;
#GET
#Produces("application/json")
public Response getUsers(#Context HttpHeaders httpHeaders, #Context UriInfo
uriInfo) {
// my function
}
}
How can I pass "httpHeaders" and "UriInfo".
My test looks like this:
Response response = target("/2/0/users/").request().get();
Users users = response.readEntity(Users.class);
assertNotNull(users);
For the service, it's good practice to either inject through the constructor or setter. This makes it easy to mock and pass in during unit testing. As for the mocking, you should use a framework like Mockito. Then you can do stuff like
MyService service = Mockito.mock(MyService.class);
when(service.getObject()).thenReturn(new Object());
HttpHeaders headers = Mockito.mock(HttpHeaders.class);
when(headers.getHeaderString("X-Header")).thenReturn("blah");
UriInfo uriInfo = Mockito.mock(UriInfo.class);
when(uriInfo.getRequestUri()).thenReturn(URI.create("http://localhost"));
Then you can just pass all these mocks to your resource class when UNIT testing.
For INTEGRATION testing you won't need to mock the headers or uriinfo. The actual ones will get passed in. But you can still mock the service if you want. Here's an example
public class MockServiceTest extends JerseyTest {
public static interface Service {
String getMessage(String name);
}
#Path("message")
public static class MessageResource {
private final Service service;
public MessageResource(Service service) {
this.service = service;
}
#GET
public String get(#QueryParam("name") String name,
#Context HttpHeaders headers,
#Context UriInfo uriInfo) {
String nameQuery = uriInfo.getQueryParameters().getFirst("name");
String header = headers.getHeaderString("X-Header");
assertNotNull(nameQuery);
assertNotNull(header);
return service.getMessage(name);
}
}
private Service service;
#Override
public ResourceConfig configure() {
service = Mockito.mock(Service.class);
return new ResourceConfig().register(new MessageResource(service));
}
#Test
public void testIt() {
Mockito.when(service.getMessage("peeskillet")).thenReturn("Hello peeskillet");
Response response = target("message").queryParam("name", "peeskillet").request()
.header("X-Header", "blah")
.get();
assertEquals(200, response.getStatus());
assertEquals("Hello peeskillet", response.readEntity(String.class));
}
}