javaspring-bootunit-testingmockitomodelmapper

Wanted but not invoked error when Testing Spring Boot Service with Mockito


I'm encountering this Wanted but not invoked error while writing tests for the PostService class using Mockito. The error message suggests that some error occurred while invoking the save method from the post repository. Here's the error message:

Wanted but not invoked:
postRepository.save(
    com.example.justatest.post.Post@6edaa77a
);
-> at com.example.justatest.post.PostServiceTest.shouldUpdatePost(PostServiceTest.java:139)

However, there were exactly 2 interactions with this mock:
postRepository.findById(
    c74dd65f-2438-4137-9d6d-a664a3f95621
);
-> at com.example.justatest.post.PostService.update(PostService.java:36)

postRepository.save(
    com.example.justatest.post.Post@5bfc257
);
-> at com.example.justatest.post.PostService.update(PostService.java:45)


Wanted but not invoked:
postRepository.save(
    com.example.justatest.post.Post@6edaa77a
);
-> at com.example.justatest.post.PostServiceTest.shouldUpdatePost(PostServiceTest.java:139)

However, there were exactly 2 interactions with this mock:
postRepository.findById(
    c74dd65f-2438-4137-9d6d-a664a3f95621
);
-> at com.example.justatest.post.PostService.update(PostService.java:36)

postRepository.save(
    com.example.justatest.post.Post@5bfc257
);
-> at com.example.justatest.post.PostService.update(PostService.java:45)

The error seems to be happening due to a mismatch between the stubbing of the modelMapper.map method and its invocation in the PostService class during the update method. I've looked into the error message, and I'm unsure how to resolve this issue. Here's the relevant code:

Service class:

@Service
@RequiredArgsConstructor
public class PostService {
    private final PostRepository postRepository;
    private final ModelMapper modelMapper;

    public Optional<PostResponseDto> update(UUID uuid, PostRequestDto postRequestDto) {
        Optional<Post> postOptional = postRepository.findById(uuid);

        if (postOptional.isEmpty()) {
            return Optional.empty();
        }

        Post post = postOptional.get();
        modelMapper.map(postRequestDto, post);

        return Optional.of(modelMapper.map(postRepository.save(post), PostResponseDto.class));
    }

    // ... (other methods)
}

Test class:

@ExtendWith(MockitoExtension.class)
class PostServiceTest {
    @Mock
    private PostRepository postRepository;

    @Mock
    private ModelMapper modelMapper;

    @InjectMocks
    private PostService postService;
    private Post post;

    @BeforeEach
    void setUp() {
        this.post = new Post(UUID.randomUUID(), "title", "description");
    }

    @Test
    void shouldUpdatePost() {
        PostRequestDto mockPostRequestDto = new PostRequestDto("Updated Title", "Updated Description");
        Post mockUpdatedPost = new Post(post.getId(), mockPostRequestDto.getTitle(), mockPostRequestDto.getDescription());
        PostResponseDto mockPostResponseDto = new PostResponseDto(mockUpdatedPost.getId(), mockUpdatedPost.getTitle(), mockUpdatedPost.getDescription());

        doReturn(Optional.of(post)).when(postRepository).findById(post.getId());
        doNothing().when(modelMapper).map(mockPostRequestDto, post);
        doReturn(mockUpdatedPost).when(postRepository).save(post);
        doReturn(mockPostResponseDto).when(modelMapper).map(mockUpdatedPost, PostResponseDto.class);

        Optional<PostResponseDto> updatedPostOptional = postService.update(post.getId(), mockPostRequestDto);

        assertTrue(updatedPostOptional.isPresent());
        PostResponseDto updatedPost = updatedPostOptional.get();

        assertEquals(mockPostResponseDto.getId(), updatedPost.getId());
        assertEquals(mockPostResponseDto.getTitle(), updatedPost.getTitle());
        assertEquals(mockPostResponseDto.getDescription(), updatedPost.getDescription());

        verify(postRepository, times(1)).findById(post.getId());
        verify(postRepository, times(1)).save(mockUpdatedPost);
        verify(modelMapper, times(1)).map(mockPostRequestDto, post);
        verify(modelMapper, times(1)).map(mockUpdatedPost, PostResponseDto.class);
    }
}

I'm unable to determine what's causing this problem and how to resolve it. Any help in understanding the issue and finding a solution would be greatly appreciated.


Solution

  • You have set up your repository to return post, which is then saved by your service method:

    // stub:
    doReturn(Optional.of(post)).when(postRepository).findById(post.getId());
    
    // service:
    Optional<Post> postOptional = postRepository.findById(uuid);
    // ...
    return Optional.of(modelMapper.map(
      postRepository.save(post), // <-- saved here
      PostResponseDto.class));
    

    Yet, you verify that save has been called with mockUpdatePost, which is a different instance:

    verify(postRepository, times(1)).save(mockUpdatedPost);
    

    This can also be seen from the error message:

    Wanted but not invoked:
    postRepository.save(
        com.example.justatest.post.Post@6edaa77a // <-- instance 6edaa77a
    );
    -> at com.example.justatest.post.PostServiceTest.shouldUpdatePost(PostServiceTest.java:139)
    
    However, there were exactly 2 interactions with this mock:
    ...
    
    postRepository.save(
        com.example.justatest.post.Post@5bfc257 // <-- instance 5bfc257
    );
    -> at com.example.justatest.post.PostService.update(PostService.java:45)
    

    Fix your verify call to expect the correct instance:

    verify(postRepository).save(post);
    

    PS. When stubbing the calls on your mock repository, you are using the correct post instance:

    doReturn(mockUpdatedPost).when(postRepository).save(post);
    

    With strict stubbing mode of Mockito (the default in newer versions), the test will fail if a stubbed method has not been called, so the verify call is actually redundant (but you might still prefer it for explicitly clarifying your intent).