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
相关产品推荐
相关产品推荐

