Kotlin Spring Boot:如何在测试中注入带@Value的组件?
问题描述
我有一个Kotlin Spring Boot应用,部分组件用@Value从application.yaml读取配置,示例代码如下:
@Component class IosClientConfig : ClientConfig { @Value("\${<spring-config-value>}") override lateinit var redirectUri: String @Value("\${<spring-config-value>}") override lateinit var clientId: String @Value("\${<spring-config-value>}") override lateinit var clientSecret: String @Value("\${<spring-config-value>}") override lateinit var tokenExchangeEndpoint: String }
这些配置对象被注入到服务层:
@Service class TokenExchangeService { val logger = LoggerFactory.getLogger(javaClass) @Autowired lateinit var iOSConfig: IosClientConfig @Autowired lateinit var androidConfig: AndroidClientConfig ... }
编写TokenExchangeService的单元测试时遇到问题:不加载完整Spring上下文的话,配置类无法自动注入,但加载全上下文违背单元测试的初衷。而且因为用了lateinit变量,没法通过构造函数注入配置类。期望的测试写法如下,但不配置全上下文无法运行:
@ExtendWith(MockKExtension::class) class TokenExchangeServiceTest() { @Autowired lateinit var tokenExchangeService: TokenExchangeService @BeforeEach fun init() { MockKAnnotations.init(this) } }
想知道有没有更易测试的组件注入方式,或者无需加载全上下文就能在测试中成功@Autowired服务的方法?
解决方案
一、从根源优化:重构组件注入与配置方式
1. 改用@ConfigurationProperties+构造函数注入
这是Spring Boot推荐的配置绑定方式,同时用构造函数注入替代字段注入,彻底解决可测试性问题:
第一步:重构配置类
用@ConfigurationProperties绑定yaml中的配置前缀,直接生成数据类:
@ConfigurationProperties(prefix = "ios.client") data class IosClientProperties( val redirectUri: String, val clientId: String, val clientSecret: String, val tokenExchangeEndpoint: String ) : ClientConfig
然后在主配置类添加@EnableConfigurationProperties(IosClientProperties::class),或者直接给配置类加@Component注解。
第二步:重构服务类为构造函数注入
@Service class TokenExchangeService( private val iOSConfig: IosClientProperties, private val androidConfig: AndroidClientProperties ) { val logger = LoggerFactory.getLogger(javaClass) ... }
这样单元测试时完全不需要Spring上下文,直接手动实例化配置和服务:
@ExtendWith(MockKExtension::class) class TokenExchangeServiceTest { // 手动创建测试用的配置实例 private val mockIosConfig = IosClientProperties( redirectUri = "test-uri", clientId = "test-id", clientSecret = "test-secret", tokenExchangeEndpoint = "test-endpoint" ) private val mockAndroidConfig = AndroidClientProperties(/* 填入测试用配置值 */) // 直接实例化服务类 private val tokenExchangeService = TokenExchangeService(mockIosConfig, mockAndroidConfig) @Test fun testTokenExchange() { // 执行测试逻辑 } }
2. 保留@Value但改用构造函数注入
如果不想替换配置绑定方式,也可以把配置类改成构造函数注入@Value:
@Component class IosClientConfig( @Value("\${ios.client.redirect-uri}") override val redirectUri: String, @Value("\${ios.client.client-id}") override val clientId: String, @Value("\${ios.client.client-secret}") override val clientSecret: String, @Value("\${ios.client.token-exchange-endpoint}") override val tokenExchangeEndpoint: String ) : ClientConfig
服务类同样用构造函数注入,测试时可以手动创建配置实例或用MockK模拟。
二、不修改原有代码:测试层面的折中方案
1. 使用Spring测试切片,只加载必要上下文
用@SpringBootTest指定仅加载需要的类,避免全量上下文,同时用@TestPropertySource提供测试配置:
@SpringBootTest(classes = [TokenExchangeService::class, IosClientConfig::class, AndroidClientConfig::class]) @TestPropertySource(properties = [ "ios.client.redirect-uri=test-uri", "ios.client.client-id=test-id", // 其他配置项 ]) @ExtendWith(MockKExtension::class) class TokenExchangeServiceTest { @Autowired lateinit var tokenExchangeService: TokenExchangeService @Test fun testTokenExchange() { // 测试逻辑 } }
2. 用@MockBean模拟配置类
如果不需要真实配置值,仅需验证服务逻辑,可以直接Mock配置类:
@ExtendWith(SpringExtension::class, MockKExtension::class) @ContextConfiguration(classes = [TokenExchangeService::class]) class TokenExchangeServiceTest { @MockBean lateinit var iOSConfig: IosClientConfig @MockBean lateinit var androidConfig: AndroidClientConfig @Autowired lateinit var tokenExchangeService: TokenExchangeService @BeforeEach fun setup() { // 模拟配置类的属性返回值 every { iOSConfig.redirectUri } returns "test-uri" every { iOSConfig.clientId } returns "test-id" // 其他属性的模拟 } @Test fun testTokenExchange() { // 测试逻辑 } }
内容的提问来源于stack exchange,提问作者D-Wire
相关产品推荐
相关产品推荐

