如何使用 SpringBoot2、JUnit5 和 Kotlin 将配置属性注入单元测试

Posted

技术标签:

【中文标题】如何使用 SpringBoot2、JUnit5 和 Kotlin 将配置属性注入单元测试【英文标题】:How can I inject config properties into a unit test, using SpringBoot2, JUnit5, and Kotlin 【发布时间】:2018-12-22 02:03:35 【问题描述】:

我的场景:

我正在构建一个使用 Kotlin 和 SpringBoot 2.0.3 的应用程序。我正在尝试在 JUnit5 中编写所有单元测试。这三个对我来说都是新的,所以我有点挣扎。

我正在使用 @ConfigurationProperties 类(而不是 @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 类,遵循 Kotlin/Spring 最佳实践将注入的类定义为构造函数参数。

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 类是作为构造参数传递的,所以我在单元测试中建立服务时必须传递它。我尝试了各种不起作用的解决方案。我发现这条信息很有帮助:

如果正在使用 Spring Boot,则可以使用 @ConfigurationProperties 而不是 @Value 注释,但目前这仅适用于 lateinit 或可为空的 var 属性(建议使用前者),因为尚不支持由构造函数初始化的不可变类。

所以这意味着我不能使用 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

【问题讨论】:

【参考方案1】:

我在以下行中看到了错误:

@ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])

请在此处放置 配置 类(而不仅仅是 bean)。

短 - 热修复测试

在主模块中创建类(如果缺少),如 VqsS3Configration(例如,在您有生产代码的模块中)

在与您的测试相同的包中创建类似 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 testgradlew.bat bootRun 行。测试将检查我们是否能够读取属性。 bootRun 将打印自动加载的属性

无聊理论

首先 - Spring 有配置类 - 它们是加载和初始化其他类所必需的。 Configuration 类的主要目的不是 Service 或 Comonent 类 - 只是创建服务、组件等。

如果我们将 Spring 应用程序加载的算法简化,那么它将是这样的:

    查找配置类 阅读它们的注释,了解应该加载的类列表(例如参考树)(以及如何加载它们) 用不同的方式加载类:

3.1。对于使用 @ConfigurationProperties 注释的类 - 将配置项放在这里

3.2。对于使用 @RestController 注释的类 - 将它们注册为休息控制器

3.N.等等……

Spring怎么理解,应该加载什么配置?

    形式上是由 Spring Boot 完成的,但我将其命名为 Spring 了解几个初始配置 - 它们可以放入类 SpringApplicationBuilder、测试注解(见上文)、XML 上下文等。对于我们的案例,我们使用测试注解和 @ContextConfiguration 属性 递归获取所有导入的配置(例如,Spring 读取 @Import 注释,然后获取子项,然后检查它们的导入等) 使用Spring Factories从jar中自动获取配置

因此,在我们的例子中,Spring 将执行如下操作:

    从测试注释中获取配置 以递归方式获取所有其他配置 将所有类加载到比赛中 开始测试

【讨论】:

如果你需要,我可以用示例创建 github repo Manushin 谢谢...我想我理解你所说的大部分内容,但我做了更改,现在测试试图在不遵守我指定的弹簧配置文件的情况下启动我的上下文. (换句话说,它使用我的默认配置文件而不是我的测试配置文件,因此尝试运行 Flyway 并初始化我的数据库) @Nephthys76,这很奇怪......我把示例应用程序放在这里 - github.com/imanushin/spring-boot2-junit5-and-kotlin-integration 。我试图最小化它,所以它只展示应用程序属性 你在 GitHub 上的代码对我有帮助,谢谢!作为其他人的注意事项,我的执行抱怨重复配置。所以,我刚刚删除了我的 ***QuestionTestConfiguration 模拟类。【参考方案2】:

好的,我花了一整天的时间,但我终于将我的应用程序属性加载到我的单元测试上下文中。我做了 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() 
...

现在我的测试中似乎有非常多的注释......所以我将仔细研究它们每个人的实际作用,看看我是否可以减少它。但至少我的属性现在被注入到我的测试上下文中。

【讨论】:

以上是关于如何使用 SpringBoot2、JUnit5 和 Kotlin 将配置属性注入单元测试的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot2---单元测试(Junit5)

springboot2单元测试和指标监控

springboot2单元测试和指标监控

springboot2单元测试和指标监控

springboot2单元测试和指标监控

如何使用 junit5 和 testcontainers 测试存储库?