问题描述
我正在编写集成测试。我有一个在 Testcontainer 中运行的 Spring Boot 2.5 应用程序。我也有运行 wiremock 的 StubRunnerExtension。
我需要 Spring Boot 应用程序来连接到存根的 wiremock 服务器。
发生错误是因为 Spring 认为 host.testcontainers.internal
是服务的名称。不是 - 这是专门为这种情况提供的特殊 Testcontainer 主机名(从 Testcontainer 连接到主机)。
wiremock 存根绝对可以运行且可连接。如果我在运行时 docker exec -it
进入容器,我可以连接到它并使用 curl http://host.testcontainers.internal
获得有效响应。
我尝试了很多很多类型的配置来禁用 Spring Boot 负载均衡器,无论是在 application.yml
、bootstrap.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
解决方法
我已经解决了这个问题
- 设置
-
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.*
道具是否重要或相关。