Spring Boot 测试:无法绑定 @ConfigurationProperties - 确保 @ConstructorBinding 尚未应用

问题描述 投票:0回答:1

在 Spring Boot 单元测试中,如何模拟 @ConstructorBinding @ConfigurationProperties 数据类?

设置

  • 两者都有
    • Kotlin 1.4.30(用于单元测试和配置类)
    • Java 15(带有 --enable-preview)(用于业务逻辑)
  • Spring 启动 2.4.2
  • 六月5.7.1
  • Mockito(mockito-内联)3.7.7
  • Maven 3.6.3_1

我想用不同的配置来测试 FtpService(一个

@Service
,其中有一个
RestTemplate
)。

FtpService 的属性来自 Kotlin 数据类 - UrlProperties - 它用

ConstructorBinding
@ConfigurationProperties
进行注释。

注意:FtpService 的构造函数从 UrlProperties 中提取属性。这意味着 UrlProperties 必须在 Spring 加载 FtpService 之前被模拟 被存根 错误

当我尝试模拟 UrlProperties 以便为不同的测试设置属性时,我要么收到错误,要么无法插入 bean

Cannot bind @ConfigurationProperties for bean 'urlProperties'. Ensure that @ConstructorBinding has not been applied to regular bean

代码
FtpService 的`@SpringBootTest` | `src/test/kotlin/com/example/FtpServiceTest.kt`

import com.example.service.FtpService import com.example.service.UrlProperties import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.context.annotation.Bean import org.springframework.test.context.ContextConfiguration @TestConfiguration @SpringBootTest(classes = [FtpService::class]) @AutoConfigureWebClient(registerRestTemplate = true) class FtpServiceTest @Autowired constructor( private val ftpService: FtpService ) { // MockBean inserted into Spring Context too late, // FtpService constructor throws NPE // @MockBean // lateinit var urlProperties: UrlProperties @ContextConfiguration class MyTestContext { // error - // > Cannot bind @ConfigurationProperties for bean 'urlProperties'. // > Ensure that @ConstructorBinding has not been applied to regular bean var urlProperties: UrlProperties = mock(UrlProperties::class.java) @Bean fun urlProperties() = urlProperties // error - // > Cannot bind @ConfigurationProperties for bean 'urlProperties'. // > Ensure that @ConstructorBinding has not been applied to regular bean // @Bean // fun urlProperties(): UrlProperties { // return UrlProperties( // UrlProperties.FtpProperties( // url = "ftp://localhost:21" // )) // } } @Test fun `test fetch file root`() { `when`(MyTestContext().urlProperties.ftp) .thenReturn(UrlProperties.FtpProperties( url = "ftp://localhost:21" )) assertEquals("I'm fetching a file from ftp://localhost:21!", ftpService.fetchFile()) } @Test fun `test fetch file folder`() { `when`(MyTestContext().urlProperties.ftp) .thenReturn(UrlProperties.FtpProperties( url = "ftp://localhost:21/user/folder" )) assertEquals("I'm fetching a file from ftp://localhost:21/user/folder!", ftpService.fetchFile()) } }



解决方法 - 手动定义每个测试

唯一的“解决方法”是手动定义所有 bean(这意味着我在测试过程中错过了 Spring Boot 的魔力),在我看来这更令人困惑。

解决方法 - 手动重新定义每个测试 | `src/test/kotlin/com/example/FtpServiceTest2.kt`

import com.example.service.FtpService import com.example.service.UrlProperties import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.Mockito.doReturn import org.mockito.Mockito.mock import org.springframework.boot.web.client.RestTemplateBuilder class FtpServiceTest2 { private val restTemplate = RestTemplateBuilder() .build() private lateinit var ftpService: FtpService private lateinit var urlProperties: UrlProperties @BeforeEach fun beforeEachTest() { urlProperties = mock(UrlProperties::class.java) `when`(urlProperties.ftp) .thenReturn(UrlProperties.FtpProperties( url = "default" )) ftpService = FtpService(restTemplate, urlProperties) } /** this is the only test that allows me to redefine 'url' */ @Test fun `test fetch file folder - redefine`() { urlProperties = mock(UrlProperties::class.java) `when`(urlProperties.ftp) .thenReturn(UrlProperties.FtpProperties( url = "ftp://localhost:21/redefine" )) // redefine the service ftpService = FtpService(restTemplate, urlProperties) assertEquals("I'm fetching a file from ftp://localhost:21/redefine!", ftpService.fetchFile()) } @Test fun `test default`() { assertEquals("I'm fetching a file from default!", ftpService.fetchFile()) } @Test fun `test fetch file root`() { `when`(urlProperties.ftp) .thenReturn(UrlProperties.FtpProperties( url = "ftp://localhost:21" )) assertEquals("I'm fetching a file from ftp://localhost:21!", ftpService.fetchFile()) } @Test fun `test fetch file folder`() { doReturn( UrlProperties.FtpProperties( url = "ftp://localhost:21/user/folder" )).`when`(urlProperties).ftp assertEquals("I'm fetching a file from ftp://localhost:21/user/folder!", ftpService.fetchFile()) } @Test fun `test fetch file folder - reset`() { Mockito.reset(urlProperties) `when`(urlProperties.ftp) .thenReturn(UrlProperties.FtpProperties( url = "ftp://localhost:21/mockito/reset/when" )) assertEquals("I'm fetching a file from ftp://localhost:21/mockito/reset/when!", ftpService.fetchFile()) } @Test fun `test fetch file folder - reset & doReturn`() { Mockito.reset(urlProperties) doReturn( UrlProperties.FtpProperties( url = "ftp://localhost:21/reset/doReturn" )).`when`(urlProperties).ftp assertEquals("I'm fetching a file from ftp://localhost:21/reset/doReturn!", ftpService.fetchFile()) } }




  春季应用 | `src/main/kotlin/com/example/MyApp.kt`

package com.example import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.runApplication @SpringBootApplication @EnableConfigurationProperties @ConfigurationPropertiesScan class MyApp fun main(args: Array<String>) { runApplication<MyApp>(*args) }




  示例@Service | `src/main/kotlin/com/example/service/FtpService.kt`

package com.example.service import org.springframework.stereotype.Service import org.springframework.web.client.RestTemplate @Service class FtpService( val restTemplate: RestTemplate, urlProperties: UrlProperties, val ftpProperties: UrlProperties.FtpProperties = urlProperties.ftp ) { fun fetchFile(): String { println(restTemplate) return "I'm fetching a file from ${ftpProperties.url}!" } }




  @ConfigurationProperties 和 @ConstructorBinding - `src/main/kotlin/com/example/service/UrlProperties.kt`

package com.example.service import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConstructorBinding @ConstructorBinding @ConfigurationProperties("url") data class UrlProperties(val ftp: FtpProperties) { data class FtpProperties( val url: String, ) }



    

spring-boot unit-testing kotlin mockito junit5
1个回答
0
投票

您可以使用

@TestPropertySource

注释提供自定义配置并提供不同的配置。通过在

@SpringBootTest
注释中提供属性可以实现类似的结果。那么您将不会创建自定义 URL 属性 bean。
另请参阅:

https://www.baeldung.com/spring-tests-override-properties

这种方法可能会很昂贵,因为将为每个具有此注释的测试类以及以下测试重新构建 Spring 上下文,这可能会为您的整体测试运行时间增加几秒。

选项2

您可以使用

@MockkBean

(来自

https://github.com/Ninja-Squad/springmockk
)模拟您的配置bean并指定返回值。这与您的解决方法不同,因为您仍然拥有完整的 Spring 上下文和只有一个模拟。 示例:

@TestConfiguration @SpringBootTest(classes = [FtpService::class]) @AutoConfigureWebClient(registerRestTemplate = true) class FtpServiceTest { @Autowired private lateinit var ftpService: FtpService @MockkBean private lateinit var urlProperties: UrlProperties @Nested inner class `scenario 1` { @BeforeEach fun setup() { every { urlProperties.ftp } returns "url for scenario 1" } } @Nested inner class `scenario 2` { @BeforeEach fun setup() { every { urlProperties.ftp } returns "url for scenario 2" } } }

在 Spring 中使用模拟也会对性能产生影响,而这个影响会比选项 1 中的小。

选项3

由于您只是测试 FTP 服务本身,因此您也可以在 Spring 测试中再次实例化。因此,对于您的测试,您创建一个使用所有 bean + 您的自定义属性的专用实例。

示例:

@TestConfiguration @SpringBootTest(classes = [FtpService::class]) @AutoConfigureWebClient(registerRestTemplate = true) class FtpServiceTest { // example for a bean from the context you want to inject but not define yourself @Autowired private lateinit var otherBean: OtherBean @Nested inner class `scenario 1` { private val urlProperties = UrlProperties(ftp = "url for scenario 1") private val ftpService = FtpService(urlProperties, otherBean) } @Nested inner class `scenario 2` { private val urlProperties = UrlProperties(ftp = "url for scenario 2") private val ftpService = FtpService(urlProperties, otherBean) } }

这将是最快的方法 - 结合 Spring 测试切片,您可能会加快速度。但它不会测试实际的有线服务。

替代方案

我不确定这对于您的用例是否可行:您还可以模拟您的 FTP 服务器(有一些现成的可用)并在每次测试运行期间提供不同的响应。这将允许您测试由 spring 构建的 bean,并且您可能能够绕过 spring 上下文重建,从而进行相当快的测试。

© www.soinside.com 2019 - 2024. All rights reserved.