Mockito 3.7.0测试Spring Boot Kotlin控制器NPE问题及测试方案咨询
报错根因
顺着错误栈和代码对应关系,三个核心问题直接触发NPE:
- 测试类的import包路径全部笔误:所有业务类的实际包名是
net.app.*,但测试类里导入的是net.web.*路径下的同名类,导致你通过@MockBean声明的smelterService、smelterMapper根本不是Spring容器注入到Controller里的实例,写的所有桩代码完全不生效。 - 桩代码失效后的连锁反应:
- Controller拿到的
smelterService是Spring为了满足依赖注入自动生成的占位mock,因为没有写匹配的桩,调用listAll()的时候默认返回null。 - Controller把这个
null值传给SmelterMapper.toDtoPage()方法,而该方法的entityPage参数在Kotlin中是显式非空类型(声明时没有加?可空标记),Kotlin编译期自动生成的参数非空校验逻辑直接抛出对应NPE。
- Controller拿到的
- 测试逻辑本身冗余:本来@WebMvcTest的测试目标是Controller层的HTTP交互,你把内部的
SmelterMapper也一起mock了,等于绕开了真实的转换逻辑,测试失去了实际验证价值。
正确的测试实现逻辑
按照分层测试的最小 mock 原则调整:
- 只mock最外层的业务依赖
SmelterService,所有内部组件(比如Mapstruct生成的SmelterMapper)直接用Spring容器加载的真实实例,不需要额外mock。 - 修正所有import包路径,保证
@MockBean注入的对象和Controller依赖的对象是同一个实例。 - 简化桩代码的匹配逻辑,避免Mockito的泛型强转导致的匹配失效问题。
修正后的可运行测试代码
package net.app.web import net.app.domain.Smelter import net.app.service.smelter.SmelterService import org.junit.jupiter.api.Test import org.mockito.Mockito import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultHandlers import org.springframework.test.web.servlet.result.MockMvcResultMatchers @WebMvcTest(controllers = [SmelterController::class]) internal class SmelterControllerTest @Autowired constructor( private val mockMvc: MockMvc, ) { companion object { private const val BASE_ENDPOINT = "/api/smelter" } // 仅mock业务层依赖,Mapper使用容器真实加载的Mapstruct实现 @MockBean private lateinit var smelterService: SmelterService @Test fun `list all smelters returns one item`() { // 构造测试实体,字段和DTO映射逻辑匹配即可 val testSmelter = Smelter().apply { id = "id" name = "name" metal = "value" } val smelterEntityPage = PageImpl(listOf(testSmelter)) // 给Service打桩,返回真实的实体分页对象 Mockito.`when`(smelterService.listAll(Mockito.any(Pageable::class.java))) .thenReturn(smelterEntityPage) // 发起GET请求,独立传参避免URL拼接转义问题 mockMvc.perform( MockMvcRequestBuilders.get("$BASE_ENDPOINT/list") .param("page", "1") .param("size", "19") .param("sort", "id") .contentType(MediaType.APPLICATION_JSON) ) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().is2xxSuccessful) .andExpect(MockMvcResultMatchers.jsonPath("$.content").exists()) .andExpect(MockMvcResultMatchers.jsonPath("$.content[0].id").value("id")) .andExpect(MockMvcResultMatchers.jsonPath("$.content[0].name").value("name")) .andExpect(MockMvcResultMatchers.jsonPath("$.content[0].metal").value("value")) // 验证Service层方法被按预期调用一次 Mockito.verify(smelterService, Mockito.times(1)) .listAll(Mockito.any(Pageable::class.java)) } }
额外注意事项
- 不要无意义扩大mock范围:所有和Controller同层的内部组件(比如DTO转换器、参数解析器)都应该用真实实例,只mock跨层的外部依赖(比如数据库、第三方服务),不然测试会变成自说自话的假验证。
- 当前测试场景下引入的
mockito-inline依赖没有实际作用,该依赖用于支持静态方法、final类的mock,本次测试不需要额外配置。 - Kotlin和Mockito搭配时,不要对
isA()做泛型强转,换成any()匹配即可,避免运行时匹配失效。
内容的提问来源于stack exchange,提问作者Damien
相关产品推荐
相关产品推荐

