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

时间:2018-07-13 16:26:47

标签: spring unit-testing spring-boot kotlin junit5

我的情况:

我正在构建一个使用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 / Spring的最佳实践,我将创建一个利用这些属性的Kotlin类,以将注入的类定义为构造函数参数。

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或可为null的var属性(建议使用前者),因为尚不支持由构造函数初始化的不可变类。

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

2 个答案:

答案 0 :(得分:0)

我在以下行中看到错误:

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

请在这里放置 configuration 类(而不只是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类-加载和初始化其他类需要它们。配置类的主要目的是代替服务类或共性类,而只需创建服务,组件等。

如果我们简化Spring应用程序负载的算法,则将是这样的:

  1. 查找配置类
  2. 阅读它们的注释,了解应加载的类列表(例如,引用树)(此外,应如何加载它们)
  3. 以不同的方式加载类:

3.1。对于使用@ConfigurationProperties注释的类,请在此处放置配置项

3.2。对于使用@RestController注释的类-将其注册为Rest控制器

3.N。等等...

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

  1. 通常由Spring Boot完成,但是我将其命名为Spring
  2. 了解几种初始配置-可以将它们放入类SpringApplicationBuilder,测试注释(请参见上文),XML上下文等中。对于我们的情况,我们使用测试注释和@ContextConfiguration属性
  3. 递归获取所有导入的配置(例如,Spring读取@Import批注,然后获取子代,然后检查其导入等)
  4. 使用Spring Factories从jar中自动获取配置

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

  1. 从测试注释中获取配置
  2. 以递归方式获取所有其他配置
  3. 将所有类加载到竞争中
  4. 开始测试

答案 1 :(得分:0)

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

现在我的测试中似乎有很多注解...因此,我将仔细研究它们各自的实际作用,并查看是否可以减少它。但是至少现在我的属性已注入到我的测试上下文中。