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

单元测试调用含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负责依赖注入
  • 必须stubcategoryRequestMapper.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,确保映射逻辑正常执行,无需手动创建实例
  • 移除不必要的CategoryService Mock,测试完整的Controller→Service→Repository调用链,更贴近真实场景

内容的提问来源于stack exchange,提问作者Jack

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 17:54:56