Home > database >  Spring Boot - Unable to mock using Mockito's given(), Optional always empty
Spring Boot - Unable to mock using Mockito's given(), Optional always empty

Time:06-22

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 log service.createUser(dto) inside the test method, its isPresent() 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 the UserService 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);
   // ...
}
  • Related