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.
To prevent this issue we will:
- Start the WireMock server in the application context initialisation phase.
- Store randomly selected TCP port into an application property (we'll implement custom property resolver).
- 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
-
In the Spring application context initialisation the
WireMockPortPropertySource
will:- Find an available TCP port.
- Start the WireMock server.
- Adds the WireMock bean into the Spring context. The bean name is based on the application property name.
- Resolve the port number into an application property.
-
The WireMock bean is autowired into a test and used to check communication, with the backend service.
-
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")));
}
}