
本文详解如何在不启动 spring 上下文的前提下,于 mockito 单元测试中启用真实的 mapstruct 映射器逻辑,避免 `@mock` 或 `calls_real_methods` 带来的空实现陷阱,确保 dto 转换行为可验证、可调试。
在基于 MapStruct 的分层架构中,Service 层常依赖 Mapper 将实体(Entity)转换为数据传输对象(DTO)。进行单元测试时,若仅用 @Mock 注入 Mapper,其所有方法默认返回 null;而使用 @Mock(answer = Answers.CALLS_REAL_METHODS) 也往往失效——因为 MapStruct 生成的实现类是编译期产物,未被 Mockito 正确代理,导致 listOfCardEntitiesToCardDto() 等方法实际未执行,最终返回空集合(如你遇到的 Expected size: 4 but was: 0 错误)。
✅ 正确解法:使用 @Spy + Mappers.get() 实例化真实 Mapper
MapStruct 提供了工厂类 org.mapstruct.factory.Mappers,其 get(Class
以下是修正后的测试配置示例:
@ExtendWith(MockitoExtension.class)
public class UserCardServiceTest {
@Mock
private MyCardRepository myCardRepository;
// ✅ 关键:用 @Spy 包裹由 Mappers.get() 创建的真实实例
@Spy
private MyMapper myMapper = Mappers.get(MyMapper.class);
@InjectMocks
private MyServiceImpl underTest;
private List listOfExpiredUserCardEntities;
@BeforeEach
void setUp() {
listOfExpiredUserCardEntities = new ArrayList<>(List.of(
getValidUserCardEntity().setExpirationDate(LocalDate.now().minusDays(1)).setBlocked(false),
getValidUserCardEntity().setExpirationDate(LocalDate.now().minusWeeks(1)).setBlocked(false),
getValidUserCardEntity().setExpirationDate(LocalDate.now().minusYears(1)).setBlocked(false),
getValidUserCardEntity().setExpirationDate(LocalDate.now().minusMonths(1)).setBlocked(false)
));
}
@Test
void shouldChangeStatusIfCardExpired() {
// 模拟仓库返回过期卡列表
given(myCardRepository.findAllByUserId(1L))
.willReturn(listOfExpiredUserCardEntities);
// 执行业务方法(此时 myMapper.listOfCardEntitiesToCardDto() 是真实调用)
List result = underTest.getActiveUserCardsById(1L);
// 断言结果非空且数量正确
assertThat(result).isNotNull().hasSize(4);
// 断言所有 DTO 的 blocked 字段均为 true(因 isCardExpired 返回 true)
assertThat(result)
.extracting(CardDto::isBlocked)
.containsOnly(true); // 更语义化的断言:全部为 true
}
} ⚠️ 注意事项与最佳实践:
- 不要使用 @Mock(answer = Answers.CALLS_REAL_METHODS):MapStruct 实现类通常无参构造且无 Spring 代理,CALLS_REAL_METHODS 对其无效,极易静默失败;
- @Spy 必须配合 Mappers.get() 初始化:直接声明 @Spy private MyMapper myMapper; 会导致字段为 null,必须显式赋值;
- 无需 @ContextConfiguration 或 @SpringBootTest:此方案纯 Mockito 驱动,轻量、快速,符合单元测试隔离原则;
-
确保 Mapper 接口已正确定义:检查 MyMapper 是否标注 @Mapper,且 listOfCardEntitiesToCardDto 方法签名与 MapStruct 支持的集合映射规则匹配(如参数为 List
,返回 List ); - 若 Mapper 依赖其他组件(如 DateFormatting):需额外 @Spy 或 @Mock 注入依赖,并通过 doReturn(...).when(spy).method() 进行必要打桩——但本例中纯数据转换,无需额外依赖。
总结而言,@Spy + Mappers.get() 是在标准 JUnit 5 + Mockito 环境中激活真实 MapStruct 行为的最简洁、最可靠方式。它既规避了 Spring Context 启动开销,又保障了映射逻辑的端到端可测性,是构建高可信度业务层单元测试的关键实践。










