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

Spring Boot+Kotlin单元测试中如何注入@Configuration配置类

解决思路:lateinit property sfProperties has not been initialized 异常排查

核心问题分析

你遇到的问题根源有两个:

  1. 测试上下文未正确加载Service Bean:你的测试类只指定了SFProperties::class作为配置类,Spring容器里并没有创建SFClientManagementServiceImpl的Bean;而且你手动new SFClientManagementServiceImpl(webClientBuilder)的方式,Spring不会为这个实例注入sfProperties。
  2. 手动实例化的类不支持Spring依赖注入:@Autowired注解只对Spring容器管理的Bean生效,手动new出来的对象,Spring不会处理它的依赖注入逻辑。

具体修复步骤

步骤1:调整测试类的上下文配置

把测试类的@SpringBootTest注解改为加载包含Service和配置类的Spring上下文:

@SpringBootTest(classes = [SFProperties::class, SFClientManagementServiceImpl::class])
class SalesforceClientManagementServiceImplTests {
    // ... 其他代码
}

如果你的测试类和主应用类在同一个包下,更简单的方式是直接用无参数的@SpringBootTest,它会自动扫描当前包及子包的所有Bean:

@SpringBootTest
class SalesforceClientManagementServiceImplTests {
    // ... 其他代码
}

步骤2:让Spring管理Service实例,而非手动new

不要手动创建SFClientManagementServiceImpl对象,而是通过@Autowired注入Spring容器中的Bean,同时替换掉Service里的webClientBuilder为模拟实例(这里用反射实现):

@SpringBootTest
class SalesforceClientManagementServiceImplTests {
    @Autowired
    lateinit var sfClientManagementServiceImpl: SFClientManagementServiceImpl

    @Test
    fun `createCase should return case id when case is created`() {
        // 构建模拟的ClientResponse
        val clientResponse: ClientResponse = ClientResponse
            .create(HttpStatus.OK)
            .header("Content-Type", "application/json")
            .body(ObjectMapper().writeValueAsString(Fakes().GetFakeCaseResponseDto())).build()
        val shortCircuitingExchangeFunction = ExchangeFunction { Mono.just(clientResponse) }
        val mockWebClientBuilder: WebClient.Builder = WebClient.builder().exchangeFunction(shortCircuitingExchangeFunction)

        // 用反射替换Service中的webClientBuilder字段
        val field = SFClientManagementServiceImpl::class.java.getDeclaredField("webClientBuilder")
        field.isAccessible = true
        field.set(sfClientManagementServiceImpl, mockWebClientBuilder)

        // 执行测试逻辑
        val caseResponseDto = sfClientManagementServiceImpl.createCase(Fakes().GetFakeCaseRequestDto())
        val response = caseResponseDto.block()
        
        assertEquals(Fakes().GetFakeCaseResponseDto().id, response?.id)
    }
}

步骤3:(更优雅的方式)使用@MockBean模拟WebClient

为了避免反射操作,你可以用Spring Boot的@MockBean来替换WebClient.Builder的Bean,Spring会自动把模拟实例注入到Service中:

@SpringBootTest
class SalesforceClientManagementServiceImplTests {
    @Autowired
    lateinit var sfClientManagementServiceImpl: SFClientManagementServiceImpl

    @MockBean
    lateinit var webClientBuilder: WebClient.Builder

    @Test
    fun `createCase should return case id when case is created`() {
        // 模拟WebClient的返回结果
        val clientResponse: ClientResponse = ClientResponse
            .create(HttpStatus.OK)
            .header("Content-Type", "application/json")
            .body(ObjectMapper().writeValueAsString(Fakes().GetFakeCaseResponseDto())).build()
        val mockExchangeFunction = ExchangeFunction { Mono.just(clientResponse) }
        val mockWebClient = WebClient.builder().exchangeFunction(mockExchangeFunction).build()

        // 指定webClientBuilder.build()返回模拟的WebClient
        `when`(webClientBuilder.build()).thenReturn(mockWebClient)

        // 执行测试
        val caseResponseDto = sfClientManagementServiceImpl.createCase(Fakes().GetFakeCaseRequestDto())
        val response = caseResponseDto.block()
        
        assertEquals(Fakes().GetFakeCaseResponseDto().id, response?.id)
    }
}

额外优化建议

你的SFProperties配置类可以改用Spring Boot的@ConfigurationProperties注解,这是更符合最佳实践的写法,不需要逐个写@Value,还支持IDE配置提示:

@Configuration
@ConfigurationProperties(prefix = "sf")
data class SFProperties(
    val baseurl: String,
    val case: CaseProperties,
    val application: ApplicationProperties,
    val address: AddressProperties
) {
    data class CaseProperties(val recordTypeId: String)
    data class ApplicationProperties(val recordTypeId: String)
    data class AddressProperties(
        val personal: PersonalAddressProperties,
        val business: BusinessAddressProperties
    ) {
        data class PersonalAddressProperties(val recordTypeId: String)
        data class BusinessAddressProperties(val recordTypeId: String)
    }
}

内容的提问来源于stack exchange,提问作者D.B

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 19:27:47