vkuzel.com

Start WireMock server in Spring integration test context initialisation

2022-06-23

When running tests in non-isolated fashion on a CI/CD server, where multiple processes are running, we have to select random TCP ports for our WireMock simulated backends. This port selection usually happens during, or before Spring context initialisation. On the other hand WireMock server is started when a test annotated with @WireMockTest is started.

In such setup, there is a delay between port selection and its use. In this period some other process may use the TCP port effectively blocking our application from using it, which results in the java.net.BindException: Address already in use error.

Integration tests lifecycle

To prevent this issue we will:

  1. Start the WireMock server in the application context initialisation phase.
  2. Store randomly selected TCP port into an application property (we'll implement custom property resolver).
  3. Stop the WireMock server after all tests are executed, preventing other processes from using the port.

Note: If the WireMock server has to be started just before a test, there is a solution of locking resolved TCP ports with sockets.

Lifecycle

  1. In the Spring application context initialisation the WireMockPortPropertySource will:

    1. Find an available TCP port.
    2. Start the WireMock server.
    3. Adds the WireMock bean into the Spring context. The bean name is based on the application property name.
    4. Resolve the port number into an application property.
  2. The WireMock bean is autowired into a test and used to check communication, with the backend service.

  3. After all tests are executed, the WireMock server stops and the port is released.

Implementation

WireMockPortPropertySource

  • Resolved property is saved into a map. This will ensure that single WireMock server is stared for one property.
  • Bean name is resolved from the property suffix. This means there can be as much WireMock servers as there are random-port.* properties.
public class PortContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        PropertySource propertySource = new WireMockPortPropertySource(applicationContext);
        applicationContext.getEnvironment().getPropertySources().addFirst(propertySource);
    }

    private static class WireMockPortPropertySource extends PropertySource<Object> {

        private static final String PROPERTY_SOURCE_NAME = "random-port";
        private static final String RANDOM_PORT_PROPERTY_PREFIX = "random-port.";

        private final Map<String, Integer> allocatedPorts = new HashMap<>();

        private final ConfigurableApplicationContext applicationContext;

        public WireMockPortPropertySource(ConfigurableApplicationContext applicationContext) {
            super(PROPERTY_SOURCE_NAME);
            this.applicationContext = applicationContext;
        }

        @Override
        public Object getProperty(String name) {
            if (name.startsWith(RANDOM_PORT_PROPERTY_PREFIX)) {
                return allocatedPorts.computeIfAbsent(name, this::registerWireMockServerBean);
            } else {
                return null;
            }
        }

        private int registerWireMockServerBean(String propertyName) {
            WireMockServer server = new WireMockServer(WireMockConfiguration.DYNAMIC_PORT);
            server.start();

            String beanName = propertyName.replace(RANDOM_PORT_PROPERTY_PREFIX, "") + "WireMockServer";
            applicationContext.getBeanFactory().registerSingleton(beanName, new WireMock(server));

            System.out.printf("%s, port=%d%n", beanName, server.port());
            return server.port();
        }
    }
}

Test properties file

In the test properties file we will have a fake backend service URL. When the random-port.backendService property is resolved, a selected port is immediately locked.

backend-service.url=#{'http://localhost:' + ${random-port.backendService}}

BackendServiceClient

@Service
public class BackendServiceClient {

    private final String url;

    public BackendServiceClient(@Value("${backend-service.url}") String url) {
        this.url = url;
    }

    public void call() {
        new RestTemplate().getForEntity(url + "/some-path", String.class);
    }
}

BackendServiceClientTest

  • The WireMock property name is used to resolve correct WireMock bean. If a different property name has to be used, we can employ @Qualifier("backendServiceWireMock") to identify a correct bean.

  • The WireMock.configureFor() static method is used to set the default WireMock instance into the static WireMock DSL.

    There is also possibility, to autowire the WireMockServer and call the DSL directly on the server instance. This would just need small change in the property resolver code to register the WireMock server bean instead of the WireMock client.

@SpringBootTest
@ContextConfiguration(initializers = PortContextInitializer.class)
class BackendServiceClientTest {

    @Autowired
    private WireMock backendServiceWireMock;

    @Autowired
    private BackendServiceClient backendServiceClient;

    @Test
    void callsBackendService() {
        WireMock.configureFor(backendServiceWireMock);
        WireMock.givenThat(get(urlPathEqualTo("/some-path"))
                .willReturn(aResponse()
                        .withStatus(200)));

        backendServiceClient.call();

        WireMock.verify(getRequestedFor(urlPathEqualTo("/some-path")));
    }
}