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

使用Mockito与ModelMapper的Service单元测试异常问题排查

Spring + Mockito单元测试问题排查与解决

问题背景

我是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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 02:25:38