Warm tip: This article is reproduced from stackoverflow.com, please click
spring-boot junit5 wiremock

Wiremock with Spring Boot JUnit 5 test: Address in Use after test run

发布于 2020-03-29 21:00:28

We have a Spring Boot 2.2.0.RELEASE application that we're testing with a JUnit 5 test class using WireMock. The test runs fine locally, but on our Jenkins, it fails with an "Address already in use" message after the test has run successfully.

Here's our spring dependencies from pom.xml:

<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>

So our test is pretty simple and looks like this:

@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(" ", "")));
    }
}

Again, this runs fine locally, and on Jenkins, the actual test case passes, but then we get the error:

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

I also enabled DEBUG logging for the Spring WireMockConfiguration, and funnily enough, then the build succeeds. It does mention "Resetting mappings for the next test to restart them. That's necessary when reusing the same context with new servers running on random ports" after the test.

This makes me think this might be some sort of race condition, but I can't say I fully grasp the overall setup.

Any pointers would be helpful.

Questioner
daniu
Viewed
513
Mateusz Rasiński 2020-01-31 18:26

It's a known issue: https://github.com/spring-cloud/spring-cloud-contract/issues/665

You have to use @DirtiesContext in all tests that use WireMock or set spring.test.context.cache.maxSize=1 in src/test/resources/spring.properties (https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#testcontext-ctx-management-caching)