单元测试调用含MapStruct映射的服务方法时遇空指针异常
问题分析与解决方案
一、Service测试空指针问题
是的,你需要对categoryRequestMapper进行stub,同时你的测试配置存在冲突:
- 同时使用
@SpringBootTest和@ExtendWith(MockitoExtension.class)会导致依赖注入逻辑失效:前者启动Spring上下文,后者是纯Mockito测试扩展,两者混用后,Mockito无法将categoryRequestMapper正确注入到CategoryService中,最终抛出空指针。 - 即使注入成功,
@Mock创建的mapper是Mockito模拟对象,所有方法默认返回null,不stubtoEntity方法的话,调用时也会得到null值。
修复后的Service测试代码
@ExtendWith(MockitoExtension.class) class CategoryServiceTest { @InjectMocks private CategoryService service; @Mock private CategoryRepository categoryRepository; @Mock private CategoryRequestMapper categoryRequestMapper; @ParameterizedTest @CsvFileSource(resources = "/data/categories.csv") void test_create(Long id, String name, Integer ordinal) { CategoryRequest request = new CategoryRequest(id, name, ordinal); // 1. Stub mapper的toEntity方法,返回非null的Category实例 Category mockCategory = new Category(); mockCategory.setId(id); mockCategory.setName(name); mockCategory.setOrdinal(ordinal); when(categoryRequestMapper.toEntity(request)).thenReturn(mockCategory); // 2. Stub repository的exists方法 when(categoryRepository.existsByNameIgnoreCase(request.getName())).thenReturn(false); // 3. 执行测试 CommandResponse response = service.create(request); // 4. 验证逻辑 verify(categoryRepository).save(mockCategory); assertEquals(id, response.getId()); } }
关键说明
- 移除
@SpringBootTest,仅保留@ExtendWith(MockitoExtension.class),让Mockito负责依赖注入 - 必须stub
categoryRequestMapper.toEntity()方法,确保返回有效的Category对象,避免业务逻辑中出现null
二、Controller测试的优化问题
你的Controller测试存在配置冲突和逻辑冗余问题,以下是修复方案:
修复后的Controller测试代码
@SpringBootTest @AutoConfigureMockMvc class CategoryControllerTest { @Autowired private MockMvc mockMvc; @MockBean private CategoryRepository categoryRepository; @MockBean private Clock clock; @SpyBean private CategoryResponseMapper categoryResponseMapper; @Test void test_findById() throws Exception { LocalDateTime dateTime = LocalDate.of(2022, 1, 1).atStartOfDay(); Instant utcInstant = dateTime.atZone(ZoneId.of("UTC")).toInstant(); // Stub Clock,避免时间相关空指针 when(clock.instant()).thenReturn(utcInstant); // 准备测试实体 Category category = new Category(); category.setId(123L); category.setName("Category"); category.setOrdinal(1); // Stub Repository when(categoryRepository.findById(123L)).thenReturn(Optional.of(category)); // 执行请求并验证响应 mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/categories/{id}", 123L)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().contentType("application/json")) .andExpect(MockMvcResultMatchers.jsonPath("$.timestamp").value(utcInstant.toEpochMilli())) .andExpect(MockMvcResultMatchers.jsonPath("$.data.id").value(123)) .andExpect(MockMvcResultMatchers.jsonPath("$.data.name").value("Category")) .andExpect(MockMvcResultMatchers.jsonPath("$.data.ordinal").value(1)); } }
关键说明
- 移除
MockMvcBuilders.standaloneSetup,改用@AutoConfigureMockMvc自动配置MockMvc,配合@SpringBootTest使用完整上下文 - 用
@MockBean替换Spring上下文中的Clock和CategoryRepository,避免依赖真实数据源 - 用
@SpyBean注入MapStruct生成的真实CategoryResponseMapperImpl,确保映射逻辑正常执行,无需手动创建实例 - 移除不必要的
CategoryServiceMock,测试完整的Controller→Service→Repository调用链,更贴近真实场景
内容的提问来源于stack exchange,提问作者Jack
相关产品推荐
相关产品推荐

