Spring MockMVC test with JSON content is throwing NullPointerException from Spring code - java

I have a small Spring MVC project. I'm writing MockMvc tests for it. Most of them work, but this one (the first one I've tried with a plain JSON body) is giving me trouble. I keep getting a NullPointerException from deep within Spring. I tried debugging through it, but eventually ran out of attached Spring source code without getting any closer to an answer.
My JSON block is captured from a live user test, which works fine. But in the test, it throws NPE. If I modify the JSON block to be malformed (IE, add an extra comma somewhere), then it throws a 400 Bad Request, as expected. Remove the extra comma, go back to NPE. Making the block invalid (IE, making a field null which is marked #NotNull in my domain object) does not give the expected 400 Bad Request. It just stays with the NPE.
All my other tests so far have been for controllers which just use query string params, and have worked fine. Also, I have one which due to browser restrictions on our customer's side must embed its JSON in a POST param (IE, "json = "{blah:blah}"), which I pull out and manually parse. That works fine, too.
Controller:
#RestController
public class SaveController {
#Autowired
private MyDao myDao;
#RequestMapping(value = "/path/to/controller", method = RequestMethod.POST)
#PreAuthorize("hasAnyRole('myRole', 'myAdminRole')")
public void updateThing(#Valid #RequestBody MyThing myThing) throws IOException {
myDao.updateMyThing(myThing);
}
}
Base test class:
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration(classes = {TestDataAccessConfiguration.class, TestApplicationConfiguration.class})
public abstract class AbstractSpringTestCase {
#Autowired
protected WebApplicationContext wac;
protected MockMvc mockMvc;
#Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
MockitoAnnotations.initMocks(this);
}
}
Test:
public class SaveControllerTest extends AbstractSpringTestCase {
#Mock
private MyDao myDao;
#InjectMocks
#Autowired
private SaveController classUnderTest;
private static final JSON = "<a big JSON string captured from (working) production>";
#Test
public void testHappyPath() throws Exception {
mockMvc.perform(post("/path/to/controller")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON))
.andExpect(status().isOk());
}
}
Stacktrace:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.NullPointerException
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:978)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:868)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842)
at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:62)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:170)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:137)
at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:145)
at SaveControllerTest.testHappyPath(SaveControllerTest.java)

In order to send a request body using Spring MockMvc you must map the #RequestBody object as a json string.
Here's an example:
SingupController:
#PostMapping("/signup")
public #ResponseBody
RealityKeeper createUser(#RequestBody SignupRequest signupRequest) {
System.out.println("SignupRequest: " + signupRequest);
String password = signupRequest.getPassword();
String username = signupRequest.getUsername();
String encoded = passwordEncoder.encode(password);
RealityKeeper realityKeeper = new RealityKeeper(username, encoded);
return repository.save(realityKeeper);
}
SignupControllerTest:
#Test
public void createUser() throws Exception {
SignupRequest signupRequest = new SignupRequest("foo", "bar");
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRAP_ROOT_VALUE, false);
ObjectWriter ow = mapper.writer().withDefaultPrettyPrinter();
String requestJson=ow.writeValueAsString(signupRequest);
mockMvc.perform(post("/api/signup")
.contentType(MediaType.APPLICATION_JSON)
.content(requestJson))
.andExpect(MockMvcResultMatchers.status().isOk());
}
Using jackson's object mapper you can turn a pojo into a json string and pass it to the mockMvc content method.
Hope this helps

Related

multipart/form-data MutlipartFile is not set through all args constructor

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.

How to unit test a cachable method

I am using Spring Cache.
I have a Spring controller and as part of the method responsible for that GET request, I have annotated it with #Cacheable(value = "customer", key = "#accountId"). In that method, it calls an API and does some business logic and then returns a DTO. With the cache annotation in place, I'm expecting on the first execution of this code will run normally but any subsequent calls later, it'll fetch the result from the cache.. that's correct right?
I'm writing a unit test to verify that the API was called once despite mocking multiple requests being sent to that controller. But the problem is that it keeps on calling the API multiple times and not calling the cache.
Unit test:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
#WebMvcTest(controllers = CustomerController.class, secure = false)
public class CustomerControllerTest {
private static final String ACCOUNT_ID = "1111";
#MockBean
private CustomerService customerService;
#MockBean
private CustomerPortAPI customerPortAPI;
#Autowired
MockMvc mockMvc;
#Before
public void setUp(){
when(customerService.getStatus(any())).thenReturn("test");
when(customerPortAPI.getAccount(any())).thenReturn(Account.builder().build());
}
#Test
public void shouldReturnCustomerDTOt() throws Exception {
when(customerService.Status(any())).thenReturn("test");
when(customerPortAPI.getAccount(ACCOUNT_ID)).thenReturn(Account.builder().accountId(ACCOUNT_ID).build());
mockMvc.perform(get("/customer/{accountId}/status", ACCOUNT_ID)
.accept(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("status").value(customer.NOT_REQUIRED.name()));
mockMvc.perform(get("/customer/{accountId}/status", ACCOUNT_ID)
.accept(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("status").value(customer.NOT_REQUIRED.name()));
verify(customerPorAPI, times(1)).getAccount(ACCOUNT_ID);
}}
Controller Method:
#Cacheable(value = "statusEligibility", key = "#customerId")
#GetMapping
public CustomerStatusDTO getCustomerStatus(#PathVariable String customerId) {
Customer customer = cusomterPort.getAccount(customerId);
Status status = service.getStatus(customer);
if (status.equals(Cons.REQUIRED)) {
/.../
} else {
/.../
}

Spring Boot #Async annotation and MockRestServiceServer

I'm using Spring Boot 2.0.6 and Java 10. I did the following service that only hits an external rest api using RestTemplate.
#Service
#Slf4j
public class DbApiClientImpl implements DbApiClient {
private final String URL_DELIMITER = "/";
private RestTemplate restTemplate;
private String url;
public DbApiClientImpl(
RestTemplateBuilder restTemplate,
#Value("${dbapi.namespace}") String namespace,
#Value("${dbapi.url}") String uri,
#Value("${dbapi.username}") String username,
#Value("${dbapi.password}") String password) {
this.restTemplate = restTemplate.basicAuthorization(username,
password).build();
this.url = namespace.concat(uri);
}
#Override
#Async("asyncExecutor")
public Merchant fetchMerchant(String id) {
ResponseEntity<Merchant> response =
restTemplate.getForEntity(url.concat(URL_DELIMITER).concat(id),
Merchant.class);
return response.getBody();
}
}
And the following test using MockeRestServiceServer:
#RunWith(SpringRunner.class)
#RestClientTest(value = {DbApiClient.class})
public class DbApiClientTest {
private static final String TEST_NAME = "test";
private static final String TEST_NAME_BAD_REQUEST = "test-
1";
private static final String TEST_NAME_SERVER_ERROR =
"test-2";
#Autowired DbApiClient dbApiClient;
#Value("${dbapi.namespace}")
private String namespace;
#Value("${dbapi.url}")
private String dbApiUrl;
#Autowired private MockRestServiceServer mockServer;
#Autowired private ObjectMapper objectMapper;
#Test
public void test() throws
JsonProcessingException, IOException {
Merchant mockMerchantSpec = populateFakeMerchant();
String jsonResponse =
objectMapper.writeValueAsString(mockMerchantSpec);
mockServer
.expect(manyTimes(),
requestTo(dbApiUrl.concat("/").concat(TEST_NAME)))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(jsonResponse,
MediaType.APPLICATION_JSON));
assertNotNull(dbApiClient.fetchMerchant(TEST_NAME));
}
The thing is that I'm getting the following exception when I run the test "No further request expected HTTP GET http://localthost... excecuted"
So seems that the #Async is borking MockerServerService response...
Also, If I commented the #Async annotation everything works just fine and I get all test green.
Thanks in advance for your comments.
Update:
As per #M.Deinum's comment. I removed the CompletableFuture from the service but I'm still getting the same exception.
The problem is your code and not your test.
If you read the documentation (the JavaDoc) of AsyncExecutionInterceptor you will see the mention that only void or Future is supported as a return type. You are returning a plain object and that is internally treated as void.
A call to that method will always respond with null. As your test is running very quickly everything has been teared down already (or is in the process of being teared down) no more calls are expected to be made.
To fix, fix your method signature and return a Future<Merchant> so that you can block and wait for the result.
#Override
#Async("asyncExecutor")
public Future<Merchant> fetchMerchant(String id) {
ResponseEntity<Merchant> response =
restTemplate.getForEntity(url.concat(URL_DELIMITER).concat(id),
Merchant.class);
return CompletableFuture.completedFuture(response.getBody());
}
Now your calling code knows about the returned Future as well as the Spring Async code. Now in your test you can now call get on the returned value (maybe with a timeout to receive an error if something fails). TO inspect the result.

Spring controller test fails with 404 when #PathVariable is a string

I am trying to test a Spring controller method - the method is at the end of the post and the test class below that. I've stripped it back a bit to try and narrow down the problem.
When I run the test as is it fails:
java.lang.AssertionError: Status
Expected :200
Actual :404
If I edit the mockMvc.perform as follows then the test passes, I don't even have to change the #PathVariables in the controller to be Longs:
mockMvc.perform(get(ApplicationEndPoints.GET_CCS_NAME_AND_ADDRESS_AJAX, 1L, 2L))
.andExpect(status().isOk());
The controller method itself works fine and returns JSON as expected. Can I just use these Long values and expect the test to be ok or how can I get it to work with Strings?
I should add that I'm a total testing noob. Thanks!
controller method:
#PreAuthorize("hasAuthority('auditor')")
#RequestMapping(value = ApplicationEndPoints.GET_APPLICANT_DATA, method = RequestMethod.GET)
#ResponseBody
public ApplicantData getNameAndAddress(#PathVariable("businessId") String businessId, #PathVariable("date") String date) {
//Date d = Date.valueOf(date);
ApplicantParams params = new ApplicantParams();
//params.setBusinessId(businessId);
//params.setApplicationReceivedDate(d);
params.setRoleId(ADDRESS_ROLE.HR.getRoleId());
return applicantService.getApplicantData(params);
}
test class:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {ApplicationTestConfig.class})
#WebAppConfiguration
public class ClientDetailAjaxControllerTest {
#InjectMocks
private ClientDetailAjaxController clientDetailAjaxController;
private MockMvc mockMvc;
private ApplicantServiceInterface<Applicant> applicantService = Mockito.mock(ApplicantServiceImpl.class, Mockito.RETURNS_DEEP_STUBS);
#Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(clientDetailAjaxController).build();
}
#Test
public void getNameAndAddress() throws Exception {
Mockito.when(applicantService.getApplicantData(Mockito.any(ApplicantParams.class)))
.thenReturn(ApplicationTestData.getApplicantData());
mockMvc.perform(get(ApplicationEndPoints.GET_APPLICANT_DATA, Mockito.anyString(), Mockito.anyString()))
.andExpect(status().isOk());
Mockito.verify(applicantService, Mockito.times(1)).getApplicantData(Mockito.any(ApplicantParams.class));
Mockito.verifyNoMoreInteractions(applicantService);
}
}
EDIT: I have cleared up one or two things in response to comments...
404 tells you that mockMVC cannot match the controller method you're trying to call.
I'm noticing that in your controller, the path is ApplicationEndPoints.GET_APPLICANT_DATA, while in the test you've shown the path is ApplicationEndPoints.GET_CCS_NAME_AND_ADDRESS_AJAX.
Nevertheless - don't use Mockito.anyString() in place of parameter values you have to supply to your RequestBuilder in the mockMVC perform method.
Here's an idea how code should look like (derived from your example):
To test this controller method:
#RequestMapping(value = "/applicant/name-and-address/", method = RequestMethod.GET)
public ApplicantData getNameAndAddress(#PathVariable("businessId") String businessId, #PathVariable("date") String date) {
// some code
}
You need to do something like:
mockMvc.perform(get("/applicant/name-and-address/{businessId}/{date}", "myBusinessId", "myDateAsString")).andExpect(status().isOk());
The difference is that you have to pass actual string values, not Mockito.anyString().
Some other notes:
If your URL path variable has the same name as the parameter you're binding it to, you can write #PathVariable String id, instead of #PathVariable("id") String id.
you can use #GetMapping in place of #RequestMapping for GET method, #PostMapping for POST etc.

A spring integration test

In my integration test, I tried to use resttemplate to send a Get request to a dummy server created by MockMvcBuilders. However I got an error:
I/O error on GET request for "http://localhost:8080/test":Connection refused:
(In the function testAccess(), url is "http://localhost:8080/test"). My code is as below:
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#IntegrationTest("server.port=8080")
public class MyTest {
private MockMvc mockMvc = null;
#Autowired
private WebApplicationContext context;
#Value("${server.port}")
private int port;
#Autowired
private MyController myController;
#Before
public void setUp(){
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.build();
}
#Test
public void testAccess() throws Exception{
RestTemplate restTemplate=new RestTemplate();
String url="http://localhost:8080/test";
try{
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
}
catch(ResourceAccessException ex){
System.out.println(ex.getMessage());
}
}
#Controller
public static class MyController {
#RequestMapping(value = "/test", method = RequestMethod.GET)
public #ResponseBody String access() {
return "OK";
}
}
}
The way I've done it is this:
First, you create a mock server from the actual RestTemplate you are using in your application
#Before
public void setup() throws Exception {
mockServer = MockRestServiceServer.createServer(myService.restTemplate);
}
Then you define how that request is going to work:
mockServer.expect(requestTo("http://localhost:8080/myrestapi"))
.andExpect(method(HttpMethod.POST))
.andRespond(withSuccess("{ success: true }", MediaType.APPLICATION_JSON));
And last you call the method in your application that will trigger a call to that url with that RestTemplate:
#Test
public void testThis() throws Exception {
myService.somethingThatCallsMyRestApi(parameters);
}
That will make your tests work as if there was a server up and running to process requests.
Using this in your example makes no sense, cause you would be testing that you build your test correctly and nothing else from the actual application.
The problem with this is that you cannot test dynamic responses. I mean, in my case the method I'm calling generates different data every time you call it and then sends it to the mockServer and then validates that the response matches in some very specific way. I haven't found a solution yet, but if the data you are going to send and receive is previously known, which would be in most cases, you'll have no problem using this.
Why are you defining a controller in your Test class and then trying to test it ? It doesn't feel logical to try to test something that is defined within the test it self.
Rather you would want to test a controller defined somewhere outside your tests, an actual controller that is used within your application.
Let's say MyController is defined as an actual controller then you could use the mockMvc object you created to test it.
mockMvc.perform(get('/test'))
.andExpect(status().isOk())

Categories