Issue
I have this Controller class that I want to test:
public class AuthController implements AuthApi {
private final UserService service;
private final PasswordEncoder encoder;
@Autowired
public AuthController(UserService service, PasswordEncoder encoder) {
this.service = service;
this.encoder = encoder;
}
@Override
public ResponseEntity<SignedInUser> register(@Valid NewUserDto newUser) {
Optional<SignedInUser> createdUser = service.createUser(newUser);
LoggerFactory.getLogger(AuthController.class).info(String.valueOf(createdUser.isPresent()));
if (createdUser.isPresent()) {
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser.get());
}
throw new InsufficientAuthentication("Insufficient info");
}
This is my unit test:
@ExtendWith(MockitoExtension.class)
@JsonTest
public class AuthControllerTest {
@InjectMocks
private AuthController controller;
private MockMvc mockMvc;
@Mock
private UserService service;
@Mock
private PasswordEncoder encoder;
private static SignedInUser testSignedInUser;
private JacksonTester<SignedInUser> signedInTester;
private JacksonTester<NewUserDto> dtoTester;
@BeforeEach
public void setup() {
ObjectMapper mapper = new AppConfig().objectMapper();
JacksonTester.initFields(this, mapper);
MappingJackson2HttpMessageConverter mappingConverter = new MappingJackson2HttpMessageConverter();
mappingConverter.setObjectMapper(mapper);
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setControllerAdvice(new RestApiErrorHandler())
.setMessageConverters(mappingConverter)
.build();
initializeTestVariables();
}
private void initializeTestVariables() {
testSignedInUser = new SignedInUser();
testSignedInUser.setId(1L);
testSignedInUser.setRefreshToken("RefreshToken");
testSignedInUser.setAccessToken("AccessToken");
}
@Test
public void testRegister() throws Exception {
NewUserDto dto = new NewUserDto();
dto.setEmail("[email protected]");
dto.setPassword("ThisIsAPassword");
dto.setName("ThisIsAName");
// Given
given(service.createUser(dto)).willReturn(Optional.of(testSignedInUser));
// When
MockHttpServletResponse res = mockMvc.perform(post("/api/v1/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(dtoTester.write(dto).getJson()).characterEncoding("utf-8").accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultHandlers.print())
.andReturn()
.getResponse();
// Then
assertThat(res.getStatus()).isEqualTo(HttpStatus.CREATED.value());
assertThat(res.getContentAsString()).isEqualTo(signedInTester.write(testSignedInUser).getJson());
}
}
Problem:
The test failed as I got the "Insufficient info" message, because isPresent() is never true, even with
given(service.createUser(dto)).willReturn(Optional.of(testSignedInUser))
already there. When I try to logservice.createUser(dto)
inside the test method, itsisPresent()
is always true. When I try to log inside the controller method, it is always false.
What I have tried:
I suspect that it is because somehow my
mockMvc
is wrongly configured so I tried to add@AutoConfigureMockMvc
but it ended up telling me that "There is no bean configured for 'mockMvc'". I tried to change theUserService
mock to the implementation class of but no use.
Please help, I'm really new into Spring's unit tests. Thank you!
Solution
The problem is as you have found the Mockito mocking:
given(service.createUser(dto)).willReturn(Optional.of(testSignedInUser))
Specifically, you instruct the mocked service
to return an Optional.of(testSignedInUser)
if it receives a parameter that is equal to dto
. However, depending on the implementation of the equals()
method of NewUserDto
, this may never occur. For example it returns true
only if the same instance is referred to instead of comparing the values of the member variables. Consequently, when passing a dto
through the mockMvc
it is first serialized and then serialized again by the object mapper, so even though its member variables have the same values, the objects are not considered equal unless you also override the equals()
method.
As an alternative, you can relax the mocking to return the Optional.of(testSignedInUser)
if any() argument is passed:
given(service.createUser(any())).willReturn(Optional.of(testSignedInUser))
or if the argument isA() specific class:
given(service.createUser(isA(NewUserDto.class))).willReturn(Optional.of(testSignedInUser))
but generally, it is preferred from a testing perspective to be explicit to avoid false positives so for this reason I advise to double check and and / or override the NewUserDto#equals()
method if possible.
Answered By - matsev
Answer Checked By - Pedro (JavaFixing Volunteer)