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!
CodePudding user response:
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.
CodePudding user response:
Thanks to @matsev answer, I was able to solve half of the problem, but there was this thing as well:
@Controller
public class AuthController implements AuthApi {
private final UserService service;
private final PasswordEncoder encoder;
// ...
}
I left the service fields as final
, which did not allow Mockito to inject or mutate the dependency. After removing final
, I got the test to work now.
EDIT: Please refer to this thread for workaround Mockito, @InjectMocks strange behaviour with final fields
EDIT 2: By design, Mockito does not inject mocks to final fields: https://github.com/mockito/mockito/issues/352
Doing so violates other APIs and can cause issues. One way to fix this is to just use constructor injection, remove @InjectMocks
, then you can just use final
fields.
// No InjectMocks
private AuthController controller;
@Mock
private UserService service;
@Mock
private PasswordEncoder encoder;
@BeforeEach
public void setup() {
controller = new AuthController(service, encoder);
// ...
}