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

Spring Boot服务层save方法单元测试返回Null对象问题排查

Spring Boot单元测试中Repository.save()返回Null的问题排查与解决

问题场景

在测试Spring Boot服务层的addTodo方法时,已MockTodoRepository并指定save方法的返回值,但实际调用后save方法始终返回Null,导致后续映射逻辑异常。

服务层代码

public TodoResponse addTodo(TodoRequest todoRequest) {
    Todo todo = modelMapper.map(todoRequest, Todo.class);
    Todo savedTodo = todoRepos.save(todo);
    TodoResponse todoResponse = modelMapper.map(savedTodo, TodoResponse.class);
    return todoResponse;
}

测试代码

@SpringBootTest
public class TodoServiceTest {
    @Mock
    private ModelMapper modelMapper;

    @InjectMocks
    private TodoService cut;

    @Mock
    private TodoRepository todoRepos;

    @Test
    @DisplayName("Test add todo")
    void should_add_todo() {
        TodoRequest todoRequest = new TodoRequest();
        todoRequest.setTitle("someValue");
        todoRequest.setDescription("someValue");
        todoRequest.setCompleted(false);

        Todo todo = new Todo(1L, todoRequest.getTitle(), todoRequest.getDescription(), todoRequest.isCompleted());

        when(modelMapper.map(todoRequest, Todo.class)).thenReturn(todo);
        when(todoRepos.save(todo)).thenReturn(todo);
        TodoResponse todoResponse = new TodoResponse();
        when(modelMapper.map(todo, TodoResponse.class)).thenReturn(todoResponse);

        TodoResponse result = cut.addTodo(todoRequest);
        verify(modelMapper).map(todoRequest, Todo.class);
        verify(todoRepos).save(todo);
        verify(modelMapper).map(todo, TodoResponse.class);

        assertNotNull(result);
        assertNotNull(result.getDescription());
        assertNotNull(result.getTitle());
        assertNotNull(result.isCompleted());
        assertEquals(todoRequest.getTitle(), result.getTitle());
        assertEquals(todoRequest.getDescription(), result.getDescription());
        assertEquals(todoRequest.isCompleted(), result.isCompleted());
    }
}

问题原因分析

核心问题是Mockito的参数匹配规则:

  • Mockito默认使用对象引用进行匹配,测试代码中when(todoRepos.save(todo)).thenReturn(todo)指定的todo是测试方法内创建的实例,但服务层中modelMapper.map(todoRequest, Todo.class)返回的对象,即使属性完全一致,也是一个新的实例(常规映射逻辑下ModelMapper会生成新对象)。
  • 由于两个Todo对象不是同一个引用,Mockito认为调用save时的参数不匹配,因此不会执行Stub逻辑,返回默认值Null。

另外,测试中创建的TodoResponse是空对象,后续断言assertNotNull(result.getDescription())等必然失败,这是额外的测试逻辑问题。

解决建议

1. 使用参数匹配器匹配任意Todo实例

这是最直接的解决方案,允许save方法接受任意Todo对象并返回预设的实例:

// 替换原有的save方法Stub
when(todoRepos.save(any(Todo.class))).thenReturn(todo);

2. 基于属性匹配(需重写equals/hashCode)

如果需要严格匹配属性而非引用,确保Todo类正确重写了equals和hashCode方法,然后使用eq匹配器:

when(todoRepos.save(eq(todo))).thenReturn(todo);

3. 修正TodoResponse的断言逻辑

测试中创建的TodoResponse为空,需要为其设置对应属性值,否则断言会失败:

TodoResponse todoResponse = new TodoResponse();
todoResponse.setTitle(todoRequest.getTitle());
todoResponse.setDescription(todoRequest.getDescription());
todoResponse.setCompleted(todoRequest.isCompleted());
when(modelMapper.map(todo, TodoResponse.class)).thenReturn(todoResponse);

4. 优化测试注解(可选)

由于是单元测试服务层,无需加载完整Spring上下文,可替换@SpringBootTest为@ExtendWith(MockitoExtension.class),提升测试运行效率:

@ExtendWith(MockitoExtension.class)
public class TodoServiceTest {
    // ... 原有代码不变
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 21:53:25