Spring Boot+Kotlin单元测试中如何注入@Configuration配置类
解决思路:
lateinit property sfProperties has not been initialized 异常排查 核心问题分析
你遇到的问题根源有两个:
- 测试上下文未正确加载Service Bean:你的测试类只指定了
SFProperties::class作为配置类,Spring容器里并没有创建SFClientManagementServiceImpl的Bean;而且你手动new SFClientManagementServiceImpl(webClientBuilder)的方式,Spring不会为这个实例注入sfProperties。 - 手动实例化的类不支持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
相关产品推荐
相关产品推荐

