How to use #MockBean and MockMvc to append some data with HTTP POST to a mocked repository - spring

I have a controller with a POST action, that creates a new BLOGPOST and return all the BLOGPOSTS including the newly created one:
#Autowired
private BlogPostInMemRepository bpr;
#RequestMapping(method = RequestMethod.POST, path="/post",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public #ResponseBody List<BlogPost> addPost(BlogPost post) {
bpr.addPost(post);
return bpr.getAllPosts();
}
The BlogPostInMemRepository code looks like this:
#Repository
public class BlogPostInMemRepository {
private List<BlogPost> posts = new ArrayList<BlogPost>(){{
add(new BlogPost(1, "Author1", "Post 1"));
add(new BlogPost(2, "Author2", "Post 2"));
}};
public List<BlogPost> getAllPosts(){
return posts;
}
public void addPost(BlogPost post){
posts.add(post);
}
}
My goal is to unit test the controller using #MockBean and MockMvc. The steps would be:
Mock the BlogPostInMemRepository injecting some initial data into it
Issue a post request with mockMvc.perform(post("/api/v1/post")
Get back the initial post with the new post successfully added.
My current tests:
#Test
public void post_whenValidParametersPassed_addsAndReturnsAllBlogPostsSuccessfully() throws Exception {
// given
BlogPost bp1 = new BlogPost(1, "John", "Post 1");
BlogPost bp2 = new BlogPost(2, "Jack", "Post 2");
List<BlogPost> posts = new ArrayList<BlogPost>(){{ add(bp1); add(bp2); }};
given(repo.getAllPosts()).willReturn(posts);
mockMvc.perform(post("/api/v1/post")
.contentType(APPLICATION_FORM_URLENCODED)
.param("id", "33")
.param("author", "Gibraltar")
.param("post", "There is now way!"))
.andExpect(status().isOk())
.andExpect(content().string("{... the whole string ...}"))
.andDo(print());
}
What I get is just the posts passed in: given(repo.getAllPosts()).willReturn(posts); - which is expected, of course.
QUESTION: how to actually inject the initial set of BLOGPOSTS, add one more with POST and get all of them back from a mocked repository?

If you are planning to mock the repository it does not really make much sense to follow your approach as the addPost will have no effect and getAllPosts would just assume it has been added. It seems a bit artificial and not bring any real value testing-wise.
What I would do here is to use a simple in-order verification:
InOrder inOrder = Mockito.inOrder(brp);
inOrder.verify(brp).addPost(any(Post.class));
inOrder.verify(brp).getAllPosts();
So to make sure the post is added before all of them are fetched from the repo.

Solved it by using doCallRealMethod() and when().thenCallRealMethod() - this seems to be the only way to inject controller data from "down-below (bottom-up)" using Mockito, as direct setters do not work on #MockBean's.
Code:
#Test
public void post_whenValidParametersPassedAndPreExistingBlogsPresent_addsAndReturnsAllBlogPostSuccessfully() throws Exception {
// given : initialize mock data
BlogPost bp1 = new BlogPost(1, "John", "Post 1");
BlogPost bp2 = new BlogPost(2, "Jack", "Post 2");
List<BlogPost> posts = new ArrayList<BlogPost>(){{ add(bp1); add(bp2); }};
// given : prep the mocked object
doCallRealMethod().when(repo).setPosts(posts);
doCallRealMethod().when(repo).addPost(any(BlogPost.class));
repo.setPosts(posts);
when(repo.getAllPosts()).thenCallRealMethod();
mockMvc.perform(post("/api/v1/post")
.contentType(APPLICATION_FORM_URLENCODED) //from MediaType
.param("id", "33")
.param("author", "Gibraltar")
.param("post", "There is now way!"))
.andExpect(status().isOk())
.andExpect(content().string("[" +
"{\"id\":1,\"author\":\"John\",\"post\":\"Post 1\"}," +
"{\"id\":2,\"author\":\"Jack\",\"post\":\"Post 2\"}," +
"{\"id\":33,\"author\":\"Gibraltar\",\"post\":\"There is now way!\"}" +
"]"))
.andDo(print());
}

Related

Junit Mock Repeated Feign Client Calls but Only the Last Mock Returned Multiple Times

I have a Spring Boot application which uses Feign Client to call a microservice to add users to the User table when a new department is created (new department will be inserted into the Department table). The request looks like:
Request:
{
"department": "math",
"usernameList": ["aaa", "bbb", "ccc"]
}
The User model:
public class User {
private String username;
}
The Feign client:
import org.springframework.cloud.openfeign.FeignClient;
#FeignClient(name = "user-client", url = "/.../user", configuration = UserConfiguration.class)
public interface UserClient {
#RequestMapping(method = RequestMethod.POST, value = "users")
User createUser(User user);
}
UserService:
#Service
public class UserService {
private final UserClient userClient;
public UserResponse createUser(#Valid Request request);
List<User> userList = request.getUsernameList()
.stream()
.map(username -> userClient.createUser(mapToUser(username))
.collect(Collectors.toList());
......
}
The above code worked and I was able to add 3 users into the database. The userList has 3 correct username. However, when I ran the junit test below, it seemed that only the last userResp ("ccc") was returned 3 times as mock response. When I ran the junit test in debug mode, I saw that each time the thenReturn(userResp) had the correct userResp, but in the UserService, the userList ended up containing 3 "ccc", rather than a list of "aaa, bbb, ccc". I tried using the FOR loop in the UserService rather than the stream, the result was the same, so it wasn't because of the stream. I also tried to remove the FOR loop in the Junit and just called the mock 3 times, same result. I am not sure if this has something to do with the Feign client mocking or if I did something wrong in my test case. Can someone please help?
My Junit:
public class UserTest {
#MockBean
private UserClient userClient;
#Test
public void testAddUser() throws Exception {
for (int i=1; i<=3; i++) {
User userResp = new User();
if (i==1) {
userResp.setUsername("aaa");
// mock response
Mockito.when(userClient.createUser(ArgumentMatchers.any(User.class)))
.thenReturn(userResp);
}
if (i==2) {
userResp.setUsername("bbb");
// mock response
Mockito.when(userClient.createUser(ArgumentMatchers.any(User.class)))
.thenReturn(userResp);
}
if (i==3) {
userResp.setUsername("ccc");
// mock response
Mockito.when(userClient.createUser(ArgumentMatchers.any(User.class)))
.thenReturn(userResp);
}
}
// invoke the real url
MvcResult result = mockMvc.perform(post("/users")
.content(TestUtils.toJson(userRequest, false))
.contentType(contentType))
.andDo(print())
.andExpect(status().isCreated())
.andReturn();
}
To make the method return different values for the subsequent call you can use
Mockito.when(userClient.createUser(ArgumentMatchers.any(User.class)))
.thenReturn("aaa")
.thenReturn("bbb")
.thenReturn("ccc"); //any
// Or a bit shorter with varargs:
Mockito.when(userClient.createUser(ArgumentMatchers.any(User.class)))
.thenReturn("aaa", "bbb", "ccc"); //any

Testing a Post multipart/form-data request on REST Controller

I've written a typical spring boot application, now I want to add integration tests to that application.
I've got the following controller and test:
Controller:
#RestController
public class PictureController {
#RequestMapping(value = "/uploadpicture", method = RequestMethod.POST)
public ResponseEntity<VehicleRegistrationData> uploadPicturePost(#RequestPart("userId") String userId, #RequestPart("file") MultipartFile file) {
try {
return ResponseEntity.ok(sPicture.saveAndParsePicture(userId, file));
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
Test:
#Test
public void authorizedGetRequest() throws Exception {
File data = ResourceUtils.getFile(testImageResource);
byte[] bytes = FileUtils.readFileToByteArray(data);
ObjectMapper objectMapper = new ObjectMapper();
MockMultipartFile file = new MockMultipartFile("file", "test.jpg", MediaType.IMAGE_JPEG_VALUE, bytes);
MockMultipartFile userId =
new MockMultipartFile("userId",
"userId",
MediaType.MULTIPART_FORM_DATA_VALUE,
objectMapper.writeValueAsString("123456").getBytes()
);
this.mockMvc.perform(multipart("/uploadPicture")
.file(userId)
.file(file)
.header(API_KEY_HEADER, API_KEY)).andExpect(status().isOk());
}
Testing the controller with the OkHttp3 client on android works seamlessly, but I can't figure out how to make that request work on the MockMvc
I expect 200 as a status code, but get 404 since, I guess, the format is not the correct one for that controller
What am I doing wrong?
It must be a typo.
In your controller, you claim the request URL to be /uploadpicture, but you visit /uploadPicture for unit test.

Handling my custom exception in Spring MVC integration test

I have the following method in a controller class:
#PostMapping("employees")
#ResponseStatus(HttpStatus.CREATED)
public Employee addEmployee(#Valid #RequestBody Employee employee) {
try {
return employeeRepository.save(employee);
} catch (DataIntegrityViolationException e) {
e.printStackTrace();
Optional<Employee> existingEmployee = employeeRepository.findByTagId(employee.getTagId());
if (!existingEmployee.isPresent()) {
//The exception root cause was not due to a unique ID violation then
throw e;
}
throw new DuplicateEntryException(
"An employee named " + existingEmployee.get().getName() + " already uses RFID tagID " + existingEmployee.get().getTagId());
}
}
Where the Employee class has a string field called tagId which has a #NaturalId annotation on it. (Please ignore that there is no dedicated service layer, this is a small and simple app).
Here is my custom DuplicateEntryException:
#ResponseStatus(HttpStatus.CONFLICT)
public class DuplicateEntryException extends RuntimeException {
public DuplicateEntryException() {
super();
}
public DuplicateEntryException(String message) {
super(message);
}
public DuplicateEntryException(String message, Throwable cause) {
super(message, cause);
}
}
Thanks to the #ResponseStatus(HttpStatus.CONFLICT) line, when I manually test the method, I get the default spring boot REST message with the timestamp, status, error, message and path fields.
I'm still getting familiar with testing in Spring and I have this test:
#Test
public void _02addEmployee_whenDuplicateTagId_thenExceptionIsReturned() throws Exception {
Employee sampleEmployee = new Employee("tagId01", "John Doe");
System.out.println("Employees in the database: " + repository.findAll().size()); //prints 1
// #formatter:off
mvc.perform(post("/employees").contentType(MediaType.APPLICATION_JSON).content(JsonUtil.toJson(sampleEmployee)))
.andExpect(status().isConflict())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.message").value("An employee named John Doe already uses RFID tagID tagId01"));
// #formatter:on
int employeeCount = repository.findAll().size();
Assert.assertEquals(1, employeeCount);
}
As you can guess, there is another test that runs first, called _01addEmployee_whenValidInput_thenCreateEmployee(), which inserts an employee with the same tagID, which is used in test #2. Test #1 passes, but test #2 does not, because the HTTP response looks like this:
MockHttpServletResponse:
Status = 409
Error message = null
Headers = {}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
And in the console before the above response, I see this:
Resolved Exception:
Type = ai.aitia.rfid_employee.exception.DuplicateEntryException
So my 2nd test fails because java.lang.AssertionError: Content type not set.
What causes the different behaviour compared to the manual testing? Why isn't this returned?
{
"timestamp": "2019-01-03T09:47:33.371+0000",
"status": 409,
"error": "Conflict",
"message": "An employee named John Doe already uses RFID tagID tagId01",
"path": "/employees"
}
Update: I experienced the same thing with a different REST endpoint as well, where the test case resulted in my own ResourceNotFoundException, but the actual JSON error object was not received by the MockMvc object.
Update2: Here are my class level annotations for the test class:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = RfidEmployeeApplication.class)
#AutoConfigureMockMvc
#AutoConfigureTestDatabase
#FixMethodOrder(MethodSorters.NAME_ASCENDING)
#TestPropertySource(locations = "classpath:application-test.properties")
org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error
fills full information for error response body, but for MockMvc it is not working. I just checked you can easily use in this case TestRestTemplate.
First just #Autowired private TestRestTemplate testRestTemplate; in test class.
and modify your test method for example:
ResponseEntity<String> response = testRestTemplate.postForEntity("/employees", sampleEmployee, String.class);
String message = com.jayway.jsonpath.JsonPath.read(response.getBody(), "$.message");
String expectedMessage = "An employee named John Doe already uses RFID tagID tagId01";
Assert.assertEquals(expectedMessage, message);
Assert.assertTrue(response.getStatusCode().is4xxClientError());
for example.

Unit testing Spring REST Controller

I have a Spring boot controller with a method like this:
// CREATE
#RequestMapping(method=RequestMethod.POST, value="/accounts")
public ResponseEntity<Account> createAccount(#RequestBody Account account,
#RequestHeader(value = "Authorization") String authorizationHeader,
UriComponentsBuilder uriBuilder) {
if (!account.getEmail().equalsIgnoreCase("")) {
account = accountService.createAccount(account);
HttpHeaders headers = new HttpHeaders();
System.out.println( "Account is null = " + (null == account)); //For debugging
headers.setLocation(uriBuilder.path("/accounts/{id}").buildAndExpand(account.getId()).toUri());
return new ResponseEntity<>(account, headers, HttpStatus.CREATED);
}
return new ResponseEntity<>(null, null, HttpStatus.BAD_REQUEST);
}
I'm trying to unit test it like this:
#Test
public void givenValidAccount_whenCreateAccount_thenSuccessed() throws Exception {
/// Arrange
AccountService accountService = mock(AccountService.class);
UriComponentsBuilder uriBuilder = mock(UriComponentsBuilder.class);
AccountController accountController = new AccountController(accountService);
Account account = new Account("someone#domain.com");
/// Act
ResponseEntity<?> createdAccount = accountController.createAccount(account, "", uriBuilder);
/// Assert
assertNotNull(createdAccount);
//assertEquals(HttpStatus.CREATED, createdAccount.getStatusCode());
}
but the account is always null. Any idea why is that ?
You may want to check my answer in How to test this method with spring boot test?
Not only you will find an answer to unit testing controllers but also how to include filters, handlers and interceptors in your test.
Hope this helps,
Jake
I think you need to when clause first of all.
when(accountController.createAccount(account, "", uriBuilder)).then(createAccount);
ResponseEntity<?> createdAccount = accountController.createAccount(account, "", uriBuilder);

How test Post request with custom object in content type application/x-www-form-urlencoded?

I have controller:
#PostMapping(value = "/value/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String updateSettings(final Dto dto) {
System.out.println(">>> " + dto);
return "template";
}
Controller works if I send request across chrome window. But when I write test for this method I get problem. Not converted object, value not inserted.
Test:
#Test
#WithMockUser(username = FAKE_VALID_USER, password = FAKE_VALID_PASSWORD)
public void test_B_CreateDtoWithValidForm() throws Exception {
final Dto dto = new Dto();
dto.setId("value");
dto.setEnabled("true");
this.mockMvc.perform(post(URL_SET_PROVIDER_SETTINGS)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.content(dto.toString()))
.andDo(print());
}
Output is >>> Dto{id=null, enabled=false}
How test Post request with custom object in content type application/x-www-form-urlencoded?
In this case you don't need to use content, but instead you need to use param in this way:
this.mockMvc.perform(post(URL_SET_PROVIDER_SETTINGS)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.param("id", "value")
.param("enabled", "true"))
.andDo(print());

Resources