TestContainer 中的 Spring Boot Cloud - 连接到没有 LoadBalancer/服务发现的 URL

Posted

技术标签:

【中文标题】TestContainer 中的 Spring Boot Cloud - 连接到没有 LoadBalancer/服务发现的 URL【英文标题】:Spring Boot Cloud in a TestContainer - connect to a URL without LoadBalancer/service discovery 【发布时间】:2021-08-25 19:31:51 【问题描述】:

我正在编写一个集成测试。我有一个在 Testcontainer 中运行的 Spring Boot 2.5 应用程序。我还有 StubRunnerExtension 运行 WireMock。

我需要 Spring Boot 应用程序来连接到存根的 WireMock 服务器。

发生错误是因为 Spring 认为 host.testcontainers.internal 是服务的名称。不是 - 它是专门为这种情况提供的特殊 Testcontainer 主机名(从 Testcontainer 连接到主机)。

Wiremock 存根肯定正在运行且可连接。如果我在运行时docker exec -it 进入容器,我可以连接到它并通过curl http://host.testcontainers.internal 获得有效响应。

我已经尝试了很多很多类型的配置来禁用 Spring Boot 负载均衡器,无论是在 application.ymlbootstrap.yml 和环境变量中。这些肯定是由 Spring 应用程序加载的 - 但它们没有做任何帮助。

ignoredInterfaces - 不会改变任何东西https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#ignore-network-interfaces SimpleDiscoveryClient - 我无法启用它 spring.cloud.discovery.client.simple.instances - 无效

尝试禁用发现客户端没有任何作用

eureka.client.enabled=false
eureka.cloud.discovery.enabled=false
spring.cloud.discovery.reactive.enabled=false
spring.cloud.discovery.blocking.enabled=false
spring.cloud.config.failFast=false

如何配置我的 Spring Boot 应用程序以连接到 URL?这必须是一个集成测试——我无法编辑应用程序的源代码。我根本不需要发现,如果我可以硬编码“service_name=http://host.testcontainers.internal”就可以了。


剩下的配置如下:

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.web.client.RestTemplate;


@Configuration
public class RestConfig 

  @Bean
  @LoadBalanced
  public RestTemplate restTemplate(RestTemplateBuilder builder) 
    return builder.build();
  


@Test
fun integrationtest() 
    // my Spring Boot server
    val server = SpringBootAppTC()
    server.start()

    // verify the stubs are running
    stubRunnerExtension.findAllRunningStubs().allServicesNames.forEach 
      logger.info("stub [$it] is running")
    
    
    assertTrue(server.isRunning)

   // assertions...


companion object 

    @JvmField
    @RegisterExtension
    val stubRunnerExtension: StubRunnerExtension = StubRunnerExtension()...



No servers available for service: host.testcontainers.internal
    at org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient.execute(BlockingLoadBalancerClient.java:79)
    at org.apache.camel.impl.engine.DefaultReactiveExecutor$Worker.schedule(DefaultReactiveExecutor.java:179)
    at org.apache.camel.processor.errorhandler.RedeliveryErrorHandler$RedeliveryTask.run(RedeliveryErrorHandler.java:712)
    at org.apache.camel.processor.errorhandler.RedeliveryErrorHandler$RedeliveryTask.doRun(RedeliveryErrorHandler.java:804)
    at org.apache.camel.component.bean.BeanProcessor.process(BeanProcessor.java:81)
    at org.apache.camel.component.bean.AbstractBeanProcessor.process(AbstractBeanProcessor.java:146)
    at org.apache.camel.component.bean.MethodInfo$1.proceed(MethodInfo.java:286)
    at org.apache.camel.component.bean.MethodInfo$1.doProceed(MethodInfo.java:316)
    at org.apache.camel.component.bean.MethodInfo.invoke(MethodInfo.java:494)
    at org.apache.camel.support.ObjectHelper.invokeMethodSafe(ObjectHelper.java:376)
    at java.base/java.lang.reflect.Method.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at my.application.RestApiService(RestApiService.java:39)
    at java.base/java.util.TimerThread.run(Unknown Source)
    at java.base/java.util.TimerThread.mainLoop(Unknown Source)
    at org.apache.camel.component.timer.TimerConsumer$1.run(TimerConsumer.java:76)
    at org.apache.camel.component.timer.TimerConsumer.sendTimerExchange(TimerConsumer.java:209)
    at org.apache.camel.impl.engine.CamelInternalProcessor.process(CamelInternalProcessor.java:398)
    at org.apache.camel.processor.Pipeline.process(Pipeline.java:184)
    at org.apache.camel.impl.engine.DefaultReactiveExecutor.scheduleMain(DefaultReactiveExecutor.java:64)
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:651)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:751)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:776)
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)
    at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
    at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:77)
    at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:93)
    at org.springframework.boot.actuate.metrics.web.client.MetricsClientHttpRequestInterceptor.intercept(MetricsClientHttpRequestInterceptor.java:86)
    at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:93)
    at org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor.intercept(LoadBalancerInterceptor.java:56)

这是我尝试在 Testcontainer 的 EnvironmentVariables 中配置 SimpleDiscoveryClient 的方法:

"SPRING_APPLICATION_JSON" to """

  "spring": 
    "cloud": 
      "discovery": 
        "client": 
          "simple": 
            "instances": 
              "contract-service": [
                
                  "uri": "http://host.testcontainers.internal:60104"
                
              ]
            
          
        
      
    
  

版本

spring-boot.version 2.5.0 spring-cloud.version 2020.0.3 spring-cloud-contract.version 3.0.3 testcontainers.version 1.15.3 junit-jupiter.version 5.7.1 java.version 11 kotlin.version 1.5.10

【问题讨论】:

SimpleDiscoveryClient 应该可以工作。你是如何定义属性的? 在 application.yml 和 bootstrap.yml 中(我在每次测试之前重建镜像),以及在 Testcontainer 的环境变量中 好的,我想我明白了 - 我将 spring.cloud.discovery.enabledfalse 更改为 true 并保留 SPRING_APPLICATION_JSON 是我的问题 显示传递给rest模板的url 你把服务名作为host放在url to rest模板http://contract-service/... 【参考方案1】:

我已经解决了这个问题

    设置
测试容器中的spring.cloud.discovery.client.simple.instances.contract-service[0].uri=http://host.testcontainers.internal:$PORT SPRING_APPLICATION_JSON (docs) spring.cloud.discovery.enabled=true eureka.client.enabled=false spring.cloud.config.failFast=false(docs,不确定快速失败是否重要)
    在 Spring Boot 容器启动前使用org.testcontainers.Testcontainers.exposeHostPorts(...) (docs) 将我的自定义 Spring Boot 应用配置设置为指向可发现的服务名称 endpoint.contract.base=http://contract-service:$PORT

编辑:我还发现,即使我禁用了 Eureka,我仍然需要这个依赖项:org.springframework.cloud:spring-cloud-starter-netflix-eureka-client。没有它,发现功能将被完全禁用,Spring Boot 应用程序可以直接连接到host.testcontainers.internal - 不用大惊小怪。

精简代码进行演示:

测试类

import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import org.slf4j.LoggerFactory
import org.springframework.cloud.contract.stubrunner.junit.StubRunnerExtension
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.output.OutputFrame
import org.testcontainers.containers.output.WaitingConsumer
import org.testcontainers.containers.wait.strategy.Wait
import org.testcontainers.junit.jupiter.Testcontainers


@Testcontainers
class MyIntegrationTest 

  init 
    // see https://www.testcontainers.org/features/networking/#exposing-host-ports-to-the-container
    org.testcontainers.Testcontainers.exposeHostPorts(contractServicePort)
  

  @Test
  fun `test Contract Service stub`() 

    // verify that the contract-service stub is running, and the hardcoded port is correct
    val contractServiceUrl = stubRunnerExtension.findStubUrl("contract-service")
    assertEquals(contractServicePort, contractServiceUrl.port)

    // manually construct the URL for contract-service
    val contractServiceDiscoverableName = "http://contract-service:$contractServicePort"
    val contractServiceUri = "http://host.testcontainers.internal:$contractServicePort"
    // (see https://www.testcontainers.org/features/networking/#exposing-host-ports-to-the-container)

    // now we have all the pieces, we can create the my SB app test container
    val mySpringBootApp =
      MySpringBootApplicationTestContainer(contractServiceDiscoverableName, contractServiceUri)
    // manually start container
    mySpringBootApp.start()
    assertTrue(mySpringBootApp.isRunning)

    // verify the stubs are running
    stubRunnerExtension.findAllRunningStubs().allServicesNames.forEach 
      logger.info("Stub '$it' is running ")
    

    val logConsumer = WaitingConsumer()
    mySpringBootApp.followOutput(logConsumer, OutputFrame.OutputType.STDOUT)
    logConsumer.waitUntil  frame ->
      frame.utf8String.contains("just putting something here so the test doesn't quit immediately and I can investigate")
    

    // todo - verification of output
  

  companion object 

    private val logger = LoggerFactory.getLogger(MyIntegrationTest::class.java)

    /** Hardcode a port for the contract-service mock */
    private const val contractServicePort: Int = 60104

    /** Download stubs from maven */
    @JvmField
    @RegisterExtension
    val stubRunnerExtension: StubRunnerExtension =
      StubRunnerExtension()
        .stubsMode(StubRunnerProperties.StubsMode.LOCAL)
        .failOnNoStubs(true)
        .downloadStub("my.company:contract-service:4.3.1")
        .withPort(contractServicePort)
  

测试容器定义

class MySpringBootApplicationTestContainer(
  private val contractServiceDiscoverableName: String,
  private val contractServiceUri: String,
  imageName: String = "my.project/my-spring-boot-application"
) : GenericContainer<MySpringBootApplicationTestContainer>(imageName) 

  init 
    waitingFor(
      Wait.forLogMessage(
        ".*Started MySpringBootApplication.*".toRegex(RegexOption.IGNORE_CASE).pattern, 1
      )
    )
  

  override fun configure() 
    super.configure()

    // set Spring Boot environment variables
    withEnv(
      mapOf(
        "spring.cloud.discovery.enabled" to "true",
        "eureka.client.enabled" to "false",

        // https://cloud.spring.io/spring-cloud-contract/reference/html/project-features.html#features-stub-runner-cloud-stubbing-profiles
        "spring.cloud.config.failFast" to "false",
        "SPRING_APPLICATION_JSON" to
            """
              
                "spring": 
                  "cloud": 
                    "discovery": 
                      "client": 
                        "simple": 
                          "instances": 
                            "contract-service": [
                              
                                "uri": "$contractServiceUri"
                              
                            ]
                          
                        
                      
                    
                  
                
              
            """.trimIndent(),

        // set the test containers
        "endpoint.contract.base" to contractServiceDiscoverableName,
      )

      // (note I'm setting environment variables in configure() because there are
      // other test containers that MySpringBootApplicationTestContainer depends on,
      // and I need to wait for them to start before fetching their URIs
    )
  

已解决的应用程序属性

如果我在 Spring Boot 启动时记录所有应用程序属性 (with this),则相关设置为:

endpoint.contract.base: http://contract-service:60104
eureka.client.enabled: false
java.vm.version: 11.0.11+9-LTS
jdk.debug: release
line.separator: 

loadbalancer.client.name: contract-service

spring.cloud.client.hostname: 574557a76be5
spring.cloud.client.ip-address: 172.17.0.5
spring.cloud.config.failFast: false
spring.cloud.discovery.client.simple.instances.contract-service[0].uri: http://host.testcontainers.internal:60104
spring.cloud.discovery.enabled: true

我不知道loadbalancer.client.name: contract-service 来自哪里或被设置在哪里。我不知道spring.cloud.client.* 道具是否重要或相关。

【讨论】:

以上是关于TestContainer 中的 Spring Boot Cloud - 连接到没有 LoadBalancer/服务发现的 URL的主要内容,如果未能解决你的问题,请参考以下文章

在 Spring Boot 集成测试中使用 TestContainer 填充数据库

Spring JUnit 5 ExtendWith TestContainer

使用 Micronaut 应用程序为 JUnit5 中的每个单元/集成测试运行一次 TestContainer

如何运行自定义 docker 镜像 testContainer

如果本地机器上没有 testcontainer,则 Springboot 测试失败

在 gitlab 管道中执行 testcontainer 集成测试