如何使用 SpringBoot2、JUnit5 和 Kotlin 将配置属性注入单元测试
How can I inject config properties into a unit test, using SpringBoot2, JUnit5, and Kotlin
我的场景:
我正在构建一个使用 Kotlin 和 SpringBoot 2.0.3 的应用程序。我试图在 JUnit5 中编写我所有的单元测试。这 3 个对我来说都是新的,所以我有点挣扎。
我正在使用 @ConfigurationProperties class(而不是@Value)将我的 application.yml 中的值注入到我的 Spring 上下文中。
@Configuration
@ConfigurationProperties(prefix = "amazon.aws.s3")
class AmazonS3Config {
val s3Enabled: Boolean = false
val region: String = ""
val accessKeyId: String = ""
val secretAccessKey: String = ""
val bucketName: String = ""
}
然后我有一个利用这些属性的 Kotlin class,遵循 Kotlin/Spring 最佳实践将注入的 class 定义为构造函数参数。
class VqsS3FileReader(val amazonS3Config: AmazonS3Config) : VqsFileReader {
companion object: mu.KLogging()
override fun getInputStream(filePath: String): InputStream {
val region: String = amazonS3Config.region
val accessKeyId: String = amazonS3Config.accessKeyId
val secretAccessKey: String = amazonS3Config.secretAccessKey
val bucketName: String = amazonS3Config.bucketName
logger.debug { "The configured s3Enabled is: $s3Enabled" }
logger.debug { "The configured region is: $region" }
logger.debug { "The configured accessKeyId is: $accessKeyId" }
logger.debug { "The configured secretAccessKey is: $secretAccessKey" }
logger.debug { "The configured bucketName is: $bucketName" }
val file: File? = File(filePath)
//This method is not yet implemented, just read a file from local disk for now
return file?.inputStream() ?: throw FileNotFoundException("File at $filePath is null")
}
}
我还没有完成这个实现,因为我正试图让单元测试先工作。所以目前,这个方法实际上并没有接触到S3,只是流式传输一个本地文件。
我的单元测试卡住了。我不知道如何将 application.yml 中的属性注入到测试上下文中。由于 ConfigProperty class 作为构造参数传递,因此我在单元测试中建立服务时必须传递它。我尝试了各种不起作用的解决方案。我找到了这条有用的信息:
If Spring Boot is being used, then @ConfigurationProperties instead of @Value annotations can be used, but currently this only works with lateinit or nullable var properties (the former is recommended) since immutable classes initialized by constructors are not yet supported.
所以这意味着我不能使用 class VqsS3FileReaderTest(amazonS3Config: AmazonS3Config): TestBase() { ... } 然后将配置传递给我的服务。
这是我目前拥有的:
@ActiveProfiles("test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
class VqsS3FileReaderTest(): TestBase() {
@Autowired
private lateinit var amazonS3Config: AmazonS3Config
@Autowired
private lateinit var fileReader: VqsS3FileReader
val filePath: String = "/fileio/sampleLocalFile.txt"
@Test
fun `can get input stream from a valid file path` () {
fileReader = VqsS3FileReader(amazonS3Config)
val sampleLocalFile: File? = getFile(filePath) //getFile is defined in the TestBase class, it just gets a file in my "resources" dir
if (sampleLocalFile != null) {
val inStream: InputStream = fileReader.getInputStream(sampleLocalFile.absolutePath)
val content: String = inStream.readBytes().toString(Charset.defaultCharset())
assert.that(content, startsWith("Lorem Ipsum"))
} else {
fail { "The file at $filePath was not found." }
}
}
}
有了这个,我的测试运行了,我的上下文似乎设置正确,但我的 application.yml 的属性没有被注入。对于我的调试输出,我看到以下内容:
08:46:43.111 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured s3Enabled is: false
08:46:43.111 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured region is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured accessKeyId is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured secretAccessKey is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured bucketName is:
全部为空字符串,这是默认值。不是我 application.yml:
中的值
amazon.aws.s3:
s3Enabled: true
region: us-west-2
accessKeyId: unknown-at-this-time
secretAccessKey: unknown-at-this-time
bucketName: test-bucket
我发现以下行有错误:
@ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
请在此处输入配置 classes(而不只是 beans)。
短路-热修复测试
在主模块中创建 class(如果缺少)如 VqsS3Configration(例如,在模块中,您有生产代码)
在与您的测试相同的包中创建 class 类似 VqsS3TestConfigration。此文件的内容:
@org.springframework.context.annotation.Configuration // mark, that this is configuration class
@org.springframework.context.annotation.Import(VqsS3Configration::class) // it references production configuration from test configuration
@org.springframework.context.annotation.ComponentScan // ask Spring to autoload all files from the package with VqsS3TestConfigration and all child packages
class VqsS3TestConfigration {
/*put test-related beans here in future*/
}
然后去测试修改声明:
@ContextConfiguration(classes = [VqsS3TestConfigration ::class]) // we ask Spring to load configuration here
我在这里创建了示例应用程序:https://github.com/imanushin/spring-boot2-junit5-and-kotlin-integration
请执行 src 文件夹中的第 .\gradlew.bat test
或 gradlew.bat bootRun
行。测试将检查我们是否能够读取属性。 bootRun 将打印自动加载的属性
无聊的理论
首先 - Spring 有配置 classes - 它们需要加载和初始化其他 classes。 Configuration classes 的主要目的不是服务或组件 classes - 只是创建服务、组件等
如果我们简化 Spring 应用程序负载的算法,那么它将是这样的:
- 查找配置 classes
- 阅读它们的注释,理解应该加载的 classes 列表(例如参考树)(以及它们应该如何加载)
- 用不同的方式加载 classes:
3.1。对于使用@ConfigurationProperties 注释的classes - 将配置项放在这里
3.2。对于使用 @RestController 注释的 classes - 将它们注册为休息控制器
3.N。等...
Spring怎么理解,应该加载什么配置?
- 正式是由 Spring Boot 完成的,但我将其命名为 Spring
- 了解几个初始配置 - 它们可以放入 class SpringApplicationBuilder、测试注释(见上文)、XML 上下文等。对于我们的案例我们使用测试注释和
@ContextConfiguration
属性
- 递归获取所有导入的配置(例如 Spring 读取
@Import
注释,然后获取子项,然后检查它们的导入等)
- 使用Spring Factories从jar自动获取配置
因此,在我们的例子中,Spring 将执行如下操作:
- 从测试注解获取配置
- 通过递归方式获取所有其他配置
- 将所有 class 加载到内容中
- 开始测试
好吧,我花了一整天的时间,但我终于让我的应用程序属性加载到我的单元测试上下文中。我做了 2 处更改:
首先,我将 @Service 注释添加到我的 VqsS3FileReader 服务中——我最初忘记了它。此外,虽然我已经更新我的测试以不通过构造函数注入 AmazonS3Config,但我忽略了更新我的服务来执行相同的操作。所以我改变了
这个:
class VqsS3FileReader(val amazonS3Config: AmazonS3Config) : VqsFileReader {
companion object: mu.KLogging()
...
对此:
@Service
class VqsS3FileReader : VqsFileReader {
companion object: mu.KLogging()
@Resource
private lateinit var amazonS3Config: AmazonS3Config
...
最后,我在测试中修改了 Spring 注释。
来自这个:
@ActiveProfiles("test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
class VqsS3FileReaderTest(): TestBase() {
...
对此:
@ActiveProfiles("test")
@SpringBootTest
@ComponentScan("com.ilmn.*")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@EnableAutoConfiguration
@SpringJUnitConfig(SpringBootContextLoader::class)
class VqsS3FileReaderTest(): TestBase() {
...
看来我现在的测试中有异常多的注释...所以我将仔细查看每个注释的实际作用,看看是否可以减少它。但至少我的属性现在被注入到我的测试上下文中。
我的场景:
我正在构建一个使用 Kotlin 和 SpringBoot 2.0.3 的应用程序。我试图在 JUnit5 中编写我所有的单元测试。这 3 个对我来说都是新的,所以我有点挣扎。
我正在使用 @ConfigurationProperties class(而不是@Value)将我的 application.yml 中的值注入到我的 Spring 上下文中。
@Configuration
@ConfigurationProperties(prefix = "amazon.aws.s3")
class AmazonS3Config {
val s3Enabled: Boolean = false
val region: String = ""
val accessKeyId: String = ""
val secretAccessKey: String = ""
val bucketName: String = ""
}
然后我有一个利用这些属性的 Kotlin class,遵循 Kotlin/Spring 最佳实践将注入的 class 定义为构造函数参数。
class VqsS3FileReader(val amazonS3Config: AmazonS3Config) : VqsFileReader {
companion object: mu.KLogging()
override fun getInputStream(filePath: String): InputStream {
val region: String = amazonS3Config.region
val accessKeyId: String = amazonS3Config.accessKeyId
val secretAccessKey: String = amazonS3Config.secretAccessKey
val bucketName: String = amazonS3Config.bucketName
logger.debug { "The configured s3Enabled is: $s3Enabled" }
logger.debug { "The configured region is: $region" }
logger.debug { "The configured accessKeyId is: $accessKeyId" }
logger.debug { "The configured secretAccessKey is: $secretAccessKey" }
logger.debug { "The configured bucketName is: $bucketName" }
val file: File? = File(filePath)
//This method is not yet implemented, just read a file from local disk for now
return file?.inputStream() ?: throw FileNotFoundException("File at $filePath is null")
}
}
我还没有完成这个实现,因为我正试图让单元测试先工作。所以目前,这个方法实际上并没有接触到S3,只是流式传输一个本地文件。
我的单元测试卡住了。我不知道如何将 application.yml 中的属性注入到测试上下文中。由于 ConfigProperty class 作为构造参数传递,因此我在单元测试中建立服务时必须传递它。我尝试了各种不起作用的解决方案。我找到了这条有用的信息:
If Spring Boot is being used, then @ConfigurationProperties instead of @Value annotations can be used, but currently this only works with lateinit or nullable var properties (the former is recommended) since immutable classes initialized by constructors are not yet supported.
所以这意味着我不能使用 class VqsS3FileReaderTest(amazonS3Config: AmazonS3Config): TestBase() { ... } 然后将配置传递给我的服务。
这是我目前拥有的:
@ActiveProfiles("test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
class VqsS3FileReaderTest(): TestBase() {
@Autowired
private lateinit var amazonS3Config: AmazonS3Config
@Autowired
private lateinit var fileReader: VqsS3FileReader
val filePath: String = "/fileio/sampleLocalFile.txt"
@Test
fun `can get input stream from a valid file path` () {
fileReader = VqsS3FileReader(amazonS3Config)
val sampleLocalFile: File? = getFile(filePath) //getFile is defined in the TestBase class, it just gets a file in my "resources" dir
if (sampleLocalFile != null) {
val inStream: InputStream = fileReader.getInputStream(sampleLocalFile.absolutePath)
val content: String = inStream.readBytes().toString(Charset.defaultCharset())
assert.that(content, startsWith("Lorem Ipsum"))
} else {
fail { "The file at $filePath was not found." }
}
}
}
有了这个,我的测试运行了,我的上下文似乎设置正确,但我的 application.yml 的属性没有被注入。对于我的调试输出,我看到以下内容:
08:46:43.111 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured s3Enabled is: false
08:46:43.111 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured region is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured accessKeyId is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured secretAccessKey is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured bucketName is:
全部为空字符串,这是默认值。不是我 application.yml:
中的值amazon.aws.s3:
s3Enabled: true
region: us-west-2
accessKeyId: unknown-at-this-time
secretAccessKey: unknown-at-this-time
bucketName: test-bucket
我发现以下行有错误:
@ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
请在此处输入配置 classes(而不只是 beans)。
短路-热修复测试
在主模块中创建 class(如果缺少)如 VqsS3Configration(例如,在模块中,您有生产代码)
在与您的测试相同的包中创建 class 类似 VqsS3TestConfigration。此文件的内容:
@org.springframework.context.annotation.Configuration // mark, that this is configuration class
@org.springframework.context.annotation.Import(VqsS3Configration::class) // it references production configuration from test configuration
@org.springframework.context.annotation.ComponentScan // ask Spring to autoload all files from the package with VqsS3TestConfigration and all child packages
class VqsS3TestConfigration {
/*put test-related beans here in future*/
}
然后去测试修改声明:
@ContextConfiguration(classes = [VqsS3TestConfigration ::class]) // we ask Spring to load configuration here
我在这里创建了示例应用程序:https://github.com/imanushin/spring-boot2-junit5-and-kotlin-integration
请执行 src 文件夹中的第 .\gradlew.bat test
或 gradlew.bat bootRun
行。测试将检查我们是否能够读取属性。 bootRun 将打印自动加载的属性
无聊的理论
首先 - Spring 有配置 classes - 它们需要加载和初始化其他 classes。 Configuration classes 的主要目的不是服务或组件 classes - 只是创建服务、组件等
如果我们简化 Spring 应用程序负载的算法,那么它将是这样的:
- 查找配置 classes
- 阅读它们的注释,理解应该加载的 classes 列表(例如参考树)(以及它们应该如何加载)
- 用不同的方式加载 classes:
3.1。对于使用@ConfigurationProperties 注释的classes - 将配置项放在这里
3.2。对于使用 @RestController 注释的 classes - 将它们注册为休息控制器
3.N。等...
Spring怎么理解,应该加载什么配置?
- 正式是由 Spring Boot 完成的,但我将其命名为 Spring
- 了解几个初始配置 - 它们可以放入 class SpringApplicationBuilder、测试注释(见上文)、XML 上下文等。对于我们的案例我们使用测试注释和
@ContextConfiguration
属性 - 递归获取所有导入的配置(例如 Spring 读取
@Import
注释,然后获取子项,然后检查它们的导入等) - 使用Spring Factories从jar自动获取配置
因此,在我们的例子中,Spring 将执行如下操作:
- 从测试注解获取配置
- 通过递归方式获取所有其他配置
- 将所有 class 加载到内容中
- 开始测试
好吧,我花了一整天的时间,但我终于让我的应用程序属性加载到我的单元测试上下文中。我做了 2 处更改:
首先,我将 @Service 注释添加到我的 VqsS3FileReader 服务中——我最初忘记了它。此外,虽然我已经更新我的测试以不通过构造函数注入 AmazonS3Config,但我忽略了更新我的服务来执行相同的操作。所以我改变了
这个:
class VqsS3FileReader(val amazonS3Config: AmazonS3Config) : VqsFileReader {
companion object: mu.KLogging()
...
对此:
@Service
class VqsS3FileReader : VqsFileReader {
companion object: mu.KLogging()
@Resource
private lateinit var amazonS3Config: AmazonS3Config
...
最后,我在测试中修改了 Spring 注释。
来自这个:
@ActiveProfiles("test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
class VqsS3FileReaderTest(): TestBase() {
...
对此:
@ActiveProfiles("test")
@SpringBootTest
@ComponentScan("com.ilmn.*")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(SpringExtension::class)
@EnableAutoConfiguration
@SpringJUnitConfig(SpringBootContextLoader::class)
class VqsS3FileReaderTest(): TestBase() {
...
看来我现在的测试中有异常多的注释...所以我将仔细查看每个注释的实际作用,看看是否可以减少它。但至少我的属性现在被注入到我的测试上下文中。