8

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.

daniu
  • 14,137
  • 4
  • 32
  • 53
  • Are you running Surefire in parallel? – ck1 Nov 25 '19 at 21:51
  • @ck1 There used to be a stray `parallel {` around the Build stage in the Jenkinsfile, but I removed it and the problem still occurs. We don't have any mention of Surefire in the pom, so we must use some default configuration (I never really fully got into how Surefire works in detail). – daniu Nov 26 '19 at 07:54

2 Answers2

4

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)

Mateusz Rasiński
  • 1,186
  • 1
  • 13
  • 15
  • It's weird because that issue is closed, and we're using a version after the one it's suppsed to be fixed :/ – daniu Feb 06 '20 at 09:56
  • Well it's not *fixed* as there is no longer a problem. It's *fixed* as we know about the problem, but the solution is to add `@DirtiesContext`. See: https://github.com/spring-cloud/spring-cloud-contract/issues/665#issuecomment-396385246 – Mateusz Rasiński Mar 13 '20 at 13:57
  • 6
    Well that's not a solution, that's a workaround. "We know about the problem so it's _fixed_" is a somewhat strange approach, I could close literally every ticket with that. – daniu Mar 18 '20 at 07:51
3

Spring-Boot: 2.5.4

Using @DirtiesContext is our last resource.

Note: Use a random port

Either:

  • Passing as annotation property: @AutoConfigureWireMock(port = 0).

Note: Keep in mind that default port is 8080, so could be a port conflict with local service or others running locally (if server.port has a default value / not specified).

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

I kindly suggest the last one, as it is more maintainable an keep config in one place to be queried.

Can reuse this on multiple endpoints (third party or local services):

app:
  endpoints:
    local-service: http://localhost:${wiremock.server.port}/some/path
    external-service: http://localhost:${wiremock.server.port}/another/path
Felix Aballi
  • 899
  • 1
  • 13
  • 31