【问题标题】:Wiremock with Spring Boot JUnit 5 test: Address in Use after test run带有 Spring Boot JUnit 5 测试的 Wiremock:测试运行后使用的地址
【发布时间】:2020-03-20 14:15:05
【问题描述】:

我们有一个 Spring Boot 2.2.0.RELEASE 应用程序,我们正在使用 WireMock 使用 JUnit 5 测试类对其进行测试。测试在本地运行良好,但在我们的 Jenkins 上,它在测试成功运行后失败并显示“地址已在使用中”消息

这是来自 pom.xml 的 spring 依赖项:

<properties>
    <java.version>11</java.version>
    <spring-cloud.version>Hoxton.RC2</spring-cloud.version>
    <spring-cloud-stream.version>3.0.0.RC2</spring-cloud-stream.version>
    <openapi.codegen.maven.plugin.version>4.1.2</openapi.codegen.maven.plugin.version>
    <jacoco-maven-plugin.version>0.8.4</jacoco-maven-plugin.version>
</properties>

<dependencies>
    <!-- Spring -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-kafka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>

    <!-- Utils -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.openapitools</groupId>
        <artifactId>openapi-generator</artifactId>
        <version>${openapi.codegen.maven.plugin.version}</version>
    </dependency>

    <!-- Testing -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-wiremock</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream</artifactId>
        <version>${spring-cloud-stream.version}</version>
        <type>test-jar</type>
        <scope>test</scope>
        <classifier>test-binder</classifier>
    </dependency>
</dependencies>

所以我们的测试非常简单,看起来像这样:

@ExtendWith(SpringExtension.class)
@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureWireMock(port = 0)
@Import(TestChannelBinderConfiguration.class)
class OurTestClass {
    @Autowired
    private OurDataCache cache;
    @Autowired
    private InputDestination source;
    @Autowired
    private OutputDestination target;

    @BeforeEach
    void setupApi() throws IOException, URISyntaxException {
        stubFor(get("/endpoint")
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
                        .withBody(data())
                )
        );
    }

    @Test
    void sampleTest() {
        String messageContent = loadResourceFileAsMessage("messageIn.json");
        String expectedOutMessage = loadResourceFileAsMessage("messageOut.json");

        Message<byte[]> message = new GenericMessage<>(messageContent.getBytes());

        source.send(message);

        Message<byte[]> received = target.receive();
        assertThat(received, notNullValue());

        assertThat(new String(received.getPayload()), equalTo(expectedOutMessage.replace(" ", "")));
    }
}

同样,这在本地运行良好,在 Jenkins 上,实际的测试用例通过了,但是我们得到了错误:

10:29:40  2019-11-25 09:29:40.683  WARN 414 --- [           main] o.s.test.context.TestContextManager      : Caught exception while invoking 'afterTestClass' callback on TestExecutionListener [org.springframework.cloud.contract.wiremock.WireMockTestExecutionListener@2b68c59b] for test class [class our.test.Class]
10:29:40
10:29:40  com.github.tomakehurst.wiremock.common.FatalStartupException: java.lang.RuntimeException: java.io.IOException: Failed to bind to /0.0.0.0:12193
10:29:40    at com.github.tomakehurst.wiremock.WireMockServer.start(WireMockServer.java:148) ~[wiremock-jre8-standalone-2.25.1.jar:na]
10:29:40    at org.springframework.cloud.contract.wiremock.WireMockConfiguration.reRegisterServer(WireMockConfiguration.java:137) ~[spring-cloud-contract-wiremock-2.2.0.RC2.jar:2.2.0.RC2]
10:29:40    at org.springframework.cloud.contract.wiremock.WireMockConfiguration.resetMappings(WireMockConfiguration.java:150) ~[spring-cloud-contract-wiremock-2.2.0.RC2.jar:2.2.0.RC2]
10:29:40    at org.springframework.cloud.contract.wiremock.WireMockTestExecutionListener.afterTestClass(WireMockTestExecutionListener.java:76) ~[spring-cloud-contract-wiremock-2.2.0.RC2.jar:2.2.0.RC2]
10:29:40    at org.springframework.test.context.TestContextManager.afterTestClass(TestContextManager.java:488) ~[spring-test-5.2.0.RELEASE.jar:5.2.0.RELEASE]
10:29:40    at org.springframework.test.context.junit.jupiter.SpringExtension.afterAll(SpringExtension.java:86) ~[spring-test-5.2.0.RELEASE.jar:5.2.0.RELEASE]
10:29:40    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeAfterAllCallbacks$13(ClassBasedTestDescriptor.java:421) ~[junit-jupiter-engine-5.5.2.jar:5.5.2]
10:29:40    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.5.2.jar:1.5.2]
10:29:40    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeAfterAllCallbacks$14(ClassBasedTestDescriptor.java:421) ~[junit-jupiter-engine-5.5.2.jar:5.5.2]
10:29:40    at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) ~[na:na]
10:29:40    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeAfterAllCallbacks(ClassBasedTestDescriptor.java:421) ~[junit-jupiter-engine-5.5.2.jar:5.5.2]
[...]
10:29:40    at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:126) ~[surefire-booter-2.22.2.jar:2.22.2]
10:29:40    at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:418) ~[surefire-booter-2.22.2.jar:2.22.2]
10:29:40  Caused by: java.lang.RuntimeException: java.io.IOException: Failed to bind to /0.0.0.0:12193
10:29:40    at com.github.tomakehurst.wiremock.jetty9.JettyHttpServer.start(JettyHttpServer.java:184) ~[wiremock-jre8-standalone-2.25.1.jar:na]
10:29:40    at com.github.tomakehurst.wiremock.WireMockServer.start(WireMockServer.java:146) ~[wiremock-jre8-standalone-2.25.1.jar:na]
10:29:40    ... 44 common frames omitted
10:29:40  Caused by: java.io.IOException: Failed to bind to /0.0.0.0:12193
10:29:40    at wiremock.org.eclipse.jetty.server.ServerConnector.openAcceptChannel(ServerConnector.java:346) ~[wiremock-jre8-standalone-2.25.1.jar:na]
[...]
10:29:40    at com.github.tomakehurst.wiremock.jetty9.JettyHttpServer.start(JettyHttpServer.java:182) ~[wiremock-jre8-standalone-2.25.1.jar:na]
10:29:40    ... 45 common frames omitted
10:29:40  Caused by: java.net.BindException: Address already in use
10:29:40    at java.base/sun.nio.ch.Net.bind0(Native Method) ~[na:na]
10:29:40    at java.base/sun.nio.ch.Net.bind(Net.java:461) ~[na:na]
10:29:40    at java.base/sun.nio.ch.Net.bind(Net.java:453) ~[na:na]
10:29:40    at java.base/sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:227) ~[na:na]
10:29:40    at java.base/sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:80) ~[na:na]
10:29:40    at wiremock.org.eclipse.jetty.server.ServerConnector.openAcceptChannel(ServerConnector.java:342) ~[wiremock-jre8-standalone-2.25.1.jar:na]
10:29:40    ... 52 common frames omitted
10:29:40
10:29:40  [ERROR] [1;31mTests [0;1mrun: [0;1m2[m, Failures: 0, [1;31mErrors: [0;1;31m1[m, Skipped: 0, Time elapsed: 15.925 s[1;31m <<< FAILURE!
10:29:40  [ERROR] our.test.Class Time elapsed: 1.843 s  <<< ERROR!
10:29:40  com.github.tomakehurst.wiremock.common.FatalStartupException: java.lang.RuntimeException: java.io.IOException: Failed to bind to /0.0.0.0:12193
10:29:40  Caused by: java.lang.RuntimeException: java.io.IOException: Failed to bind to /0.0.0.0:12193
10:29:40  Caused by: java.io.IOException: Failed to bind to /0.0.0.0:12193
10:29:40  Caused by: java.net.BindException: Address already in use

我还为 Spring WireMockConfiguration 启用了 DEBUG 日志记录,有趣的是,构建成功。它确实提到了“为下一次测试重置映射以重新启动它们。在测试后重新使用与运行在随机端口上的新服务器相同的上下文时,这是必要的”。

这让我觉得这可能是某种比赛条件,但我不能说我完全掌握了整体设置。

任何指针都会有所帮助。

【问题讨论】:

  • 您是否在并行运行 Surefire?
  • @ck1 以前在Jenkinsfile的Build阶段周围有一个杂散的parallel {,但我删除了它,问题仍然存在。我们在 pom 中没有提到 Surefire,因此我们必须使用一些默认配置(我从未真正完全了解 Surefire 的工作原理)。

标签: spring-boot junit5 wiremock


【解决方案1】:

Spring-Boot2.5.4

使用@DirtiesContext 是我们最后的资源。

注意:使用随机端口

要么:

  • 作为注解属性传递:@AutoConfigureWireMock(port = 0)

注意:请记住,默认端口是8080,因此可能与本地服务或本地运行的其他端口冲突(如果server.portdefault value / not specified)。

  • application-test.properties/.yml:wiremock.server.port=0

我建议最后一个,因为它更易于维护,将配置保留在一个待查询的地方。

可以在多个端点(第三方或本地服务)上重用它:

app:
  endpoints:
    local-service: http://localhost:${wiremock.server.port}/some/path
    external-service: http://localhost:${wiremock.server.port}/another/path

【讨论】:

    【解决方案2】:

    这是一个已知问题:https://github.com/spring-cloud/spring-cloud-contract/issues/665

    您必须在所有使用 WireMock 的测试中使用 @DirtiesContext 或在 src/test/resources/spring.properties 中设置 spring.test.context.cache.maxSize=1 (https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#testcontext-ctx-management-caching)

    【讨论】:

    • 这很奇怪,因为该问题已关闭,我们使用的是应该修复的版本之后的版本:/
    • 嗯,它不是固定,因为不再有问题。正如我们所知,它已修复,但解决方案是添加@DirtiesContext。见:github.com/spring-cloud/spring-cloud-contract/issues/…
    • 嗯,这不是一个解决方案,这是一个解决方法。 “我们知道这个问题,所以它修复了”是一种有点奇怪的方法,我可以用它来关闭每张票。
    猜你喜欢
    • 2021-09-05
    • 2019-05-21
    • 2020-07-27
    • 1970-01-01
    • 2019-02-19
    • 1970-01-01
    • 2020-07-13
    • 2018-01-04
    • 2020-10-23
    相关资源
    最近更新 更多