You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Mockito测试Spring Boot PostService时遇Wanted but not invoked错误

Mockito测试PostService时出现"Wanted but not invoked"错误

错误信息

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)

相关代码

Service类

@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类

@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);
    }
}

问题原因及解决方案

核心原因

测试中的verify(postRepository, times(1)).save(mockUpdatedPost);语句存在对象不匹配问题:

  • Service的update方法里,调用postRepository.save(post)时,传入的是从findById获取到的原Post实例(即使经过modelMapper修改,仍是同一个对象引用)
  • 但测试中验证的是手动创建的mockUpdatedPost实例,这是另一个独立的Post对象,Mockito默认通过对象引用(或equals方法)匹配调用,因此判定为未调用期望的方法,抛出错误。

解决方案

方案1:修改verify语句匹配实际传入的对象

直接将verify语句中的mockUpdatedPost替换为原post对象:

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

方案2:使用ArgumentCaptor捕获参数并验证属性

这种方式更灵活,能验证save的对象属性是否符合预期,同时避免对象引用匹配的问题:

  1. 在测试类中添加ArgumentCaptor:
@Captor
private ArgumentCaptor<Post> postCaptor;
  1. 修改verify逻辑,捕获save的参数并验证属性:
// 替换原有的save verify语句
verify(postRepository, times(1)).save(postCaptor.capture());
Post savedPost = postCaptor.getValue();
// 验证属性是否正确更新
assertEquals(mockUpdatedPost.getTitle(), savedPost.getTitle());
assertEquals(mockUpdatedPost.getDescription(), savedPost.getDescription());

额外优化:模拟ModelMapper的真实行为

当前测试中doNothing().when(modelMapper).map(mockPostRequestDto, post);只是空实现,原post对象的属性并未被修改,和真实业务逻辑不符。可以修改桩代码,模拟ModelMapper的属性复制行为:

// 替换原有的modelMapper map桩代码
doAnswer(invocation -> {
    Post targetPost = invocation.getArgument(1);
    targetPost.setTitle(mockPostRequestDto.getTitle());
    targetPost.setDescription(mockPostRequestDto.getDescription());
    return null;
}).when(modelMapper).map(mockPostRequestDto, post);

内容的提问来源于stack exchange,提问作者Eduardo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 22:05:58