使用Mockito与ModelMapper的Service单元测试异常问题排查
问题背景
我是Spring与Mockito新手,在编写单元测试时遇到几个问题:
- 实现了
BookService,功能是从BookRepository获取书籍列表,通过ModelMapper转换为BookDto返回 - 初始测试时误将断言对象设为原始
books集合而非Service返回的fetchedBooks,导致测试未按预期失败 - 修正断言对象后,运行测试出现
java.lang.AssertionError: Expecting actual not to be null,报错点在断言fetchedBooks.get(1).getTitle()处 - 尝试用
Mockito.doReturn(bookDtos.get(2)).when(mapper).map(book, BookDto.class)强制mapper返回固定的第三个BookDto,期望测试因断言不匹配失败,但未生效
使用技术栈:Spring Boot 2.7.12、Mockito 5.3.1、Assertj 3.24.2、ModelMapper 3.1.1、JUnit 5、Java 17
逐个问题解决
1. 断言对象错误问题
这个是基础逻辑错误,只要把断言目标改成Service的返回结果fetchedBooks即可,重点解决后面两个核心问题。
2. Expecting actual not to be null错误
这个错误说明fetchedBooks.get(1)的title字段为null,常见原因及解决方法:
原因:ModelMapper映射不生效
ModelMapper默认只映射字段名完全匹配且有getter/setter的属性,如果Book和BookDto字段名不一致(比如Book是bookTitle,BookDto是title),或者字段无getter/setter,就会导致映射后字段为null。
解决:
- 检查字段名是否一致,不一致则手动配置映射规则:
modelMapper.typeMap(Book.class, BookDto.class) .addMapping(Book::getBookTitle, BookDto::setTitle);
- 确保实体类字段都有对应的getter/setter方法。
原因:Mockito对ModelMapper的Mock不完整
如果只给部分Book对象配置了mock返回值,当Service处理未配置的Book时,mapper会返回null,导致对应的BookDto字段为空。
解决:
用参数匹配器让mock覆盖所有Book对象的映射请求:
when(mapper.map(any(Book.class), eq(BookDto.class))) .thenAnswer(invocation -> { Book inputBook = invocation.getArgument(0); // 从预设的bookDtos中匹配对应id的Dto返回 return bookDtos.stream() .filter(dto -> dto.getId().equals(inputBook.getId())) .findFirst() .orElse(null); });
3. Mockito指定固定返回值未生效问题
问题出在mock的参数匹配上:when(mapper).map(book, BookDto.class)中的book是具体对象,只有当Service传入的Book和这个对象equals匹配时,mock才会返回指定值。如果Service处理的是列表中另一个Book,这个规则不会生效。
解决:
如果要让所有map调用都返回固定BookDto,用参数匹配器any():
doReturn(bookDtos.get(2)).when(mapper).map(any(Book.class), eq(BookDto.class));
这样无论Service传入哪个Book,mapper都会返回第三个BookDto,此时断言fetchedBooks.get(1).getTitle()等于第二个Dto的title就会失败,符合预期。
完整测试示例
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.modelmapper.ModelMapper; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; @ExtendWith(MockitoExtension.class) class BookServiceTest { @Mock private BookRepository bookRepository; @Mock private ModelMapper modelMapper; @InjectMocks private BookService bookService; @Test void shouldHandleBookMappingCorrectly() { // 测试数据准备 Book book1 = new Book(1L, "Spring实战", "Craig Walls"); Book book2 = new Book(2L, "Mockito实践", "Tom Mock"); List<Book> books = List.of(book1, book2); BookDto bookDto1 = new BookDto(1L, "Spring实战", "Craig Walls"); BookDto bookDto2 = new BookDto(2L, "Mockito实践", "Tom Mock"); BookDto bookDto3 = new BookDto(3L, "JUnit5指南", "Sam Test"); List<BookDto> bookDtos = List.of(bookDto1, bookDto2, bookDto3); // Mock Repository返回书籍列表 doReturn(books).when(bookRepository).findAll(); // 正常映射场景 doReturn(bookDto1).when(modelMapper).map(book1, BookDto.class); doReturn(bookDto2).when(modelMapper).map(book2, BookDto.class); List<BookDto> fetchedBooks = bookService.getAllBooks(); assertThat(fetchedBooks.get(0).getTitle()).isEqualTo("Spring实战"); assertThat(fetchedBooks.get(1).getTitle()).isEqualTo("Mockito实践"); // 强制返回固定Dto,测试断言失败场景 doReturn(bookDto3).when(modelMapper).map(any(Book.class), eq(BookDto.class)); fetchedBooks = bookService.getAllBooks(); // 此处断言会失败,实际返回的是"JUnit5指南" assertThat(fetchedBooks.get(1).getTitle()).isEqualTo("Mockito实践"); } }
内容的提问来源于stack exchange,提问作者10101101

