You need to enable JavaScript to run this app. Mockito 3.7.0测试Spring Boot Kotlin控制器NPE问题及测试方案咨询
优惠活动
大模型
产品
解决方案
定价
更多

Mockito 3.7.0测试Spring Boot Kotlin控制器NPE问题及测试方案咨询

报错根因

顺着错误栈和代码对应关系,三个核心问题直接触发NPE:

  • 测试类的import包路径全部笔误:所有业务类的实际包名是net.app.*,但测试类里导入的是net.web.*路径下的同名类,导致你通过@MockBean声明的smelterService、smelterMapper根本不是Spring容器注入到Controller里的实例,写的所有桩代码完全不生效。
  • 桩代码失效后的连锁反应:
    1. Controller拿到的smelterService是Spring为了满足依赖注入自动生成的占位mock,因为没有写匹配的桩,调用listAll()的时候默认返回null。
    2. Controller把这个null值传给SmelterMapper.toDtoPage()方法,而该方法的entityPage参数在Kotlin中是显式非空类型(声明时没有加?可空标记),Kotlin编译期自动生成的参数非空校验逻辑直接抛出对应NPE。
  • 测试逻辑本身冗余:本来@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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.09 23:15:05