On a CI/CD server, where processes cannot be isolated from one of each other, we have to select random TCP ports for WireMock servers.
Port number must be selected at the start of the application, to be available in the Spring application properties. On the other hand in tests annotated with the @WireMockTest
annotation, a WireMock server is started when a test starts.
In a time period between port selection a WireMock server start ("risk period"), an other process can open a connection on selected port, effectively blocking WireMock server from starting. This usually ends in the java.net.BindException: Address already in use
error.
To mitigate this issue, we will lock selected port by opening a socket, effectively blocking other processes from using it.
When WireMock tests starts, the blocking socket will be closed, so the WireMock server can use it. After a test is finished, socked is opened again, to protect the port for us in another tests.
This process will continue until all tests are executed and the application stops.
Note: There is a simplified solution, which starts a WireMock server for each port during the application context init and stops it at the end of all tests.
Components
PortPool
holds a list of open sockets, thus blocked ports.PortLockingPropertySource
property resolver, child class of custom application context initialiser.WireMockPortLockingExtension
JUnit 5 extension, which (un)locks ports and starts WireMock server during a test lifecycle.
Lifecycle
-
During the Spring application context initialisation
PortLockingPropertySource
will resolve properties starting with therandom-port.
prefix. Part of the property name after the prefix (port name) is used to identify port in port pool, and to resolve correct WireMockServer.For a resolved property,
PortPool.lockRandomPort()
finds an available port, and opens a socket. Pool identifies a port by its port name. -
When a test starts,
WireMockPortLockingExtension.resolveParameter()
resolves aWireMockServer
test parameter, and based on parameter name finds a locked port in the pool. This assumes WireMock server parameter name must be the same as port name.The port is released by
PortPool.releasePort()
method, and newWireMockServer
is started for each parameter. -
After completion of a test, the
WireMockPortLockingExtension.afterEach()
will stop all running servers and lock all unlocked ports viaPortPool.lockPort()
. -
The application will repeat steps 2. and 3. untill all tests are finished. Then the resources are automatically released.
Nope: For sake of brevity, the provided implementation example, is not thread safe. To run tests in parallel, more thought must be put into synchronisation.
Implementation
PortPool
The pool is created as a singleton and is accessed via the INSTANCE
constant.
With a bit of work, the pool can be for example placed into the test Spring context, and accessed in the test extension by the SpringExtension.getApplicationContext(extensionContext).getBean()
method.
public class PortPool {
// Port = 0, an operating system will select a random port
public static final int RANDOM_PORT = 0;
public static final PortPool INSTANCE = new PortPool();
private final Map<String, ServerSocket> locks = new HashMap<>();
// Find and lock available port
public synchronized int lockRandomPort(String name) {
return locks.computeIfAbsent(name, (n) -> openSocket(RANDOM_PORT)).getLocalPort();
}
// Return true if a port was previously locked
public synchronized boolean hasPort(String name) {
return locks.containsKey(name);
}
// Open socked for a port
public synchronized void lockPort(String name) {
locks.compute(name, (n, socket) -> openSocket(socket.getLocalPort()));
}
// Close socket for a port
public synchronized int releasePort(String name) {
ServerSocket socket = locks.get(name);
return closeSocket(socket).getLocalPort();
}
private ServerSocket openSocket(int port) {
try {
return ServerSocketFactory.getDefault().createServerSocket(port);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private ServerSocket closeSocket(ServerSocket socket) {
try {
socket.close();
} catch (IOException ignored) {
}
return socket;
}
}
PortLockingPropertySource
public class PortLockingContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
PropertySource propertySource = new PortLockingPropertySource();
applicationContext.getEnvironment().getPropertySources().addFirst(propertySource);
}
private static class PortLockingPropertySource 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<>();
public PortLockingPropertySource() {
super(PROPERTY_SOURCE_NAME);
}
@Override
public Object getProperty(String propertyName) {
if (!propertyName.startsWith(RANDOM_PORT_PROPERTY_PREFIX)) {
return null;
}
String name = propertyName.replace(RANDOM_PORT_PROPERTY_PREFIX, "");
return allocatedPorts.computeIfAbsent(name, PortPool.INSTANCE::lockRandomPort);
}
}
}
WireMockPortLockingExtension
The parameter resolver will inject a WireMockServer
instance into a test. The name of the parameter is used to pick a correct port from the pool.
Main advantages of this solution are 1. possibility for multiple running servers at the same time, and 2. servers are started only for tests for which are needed.
public class WireMockPortLockingExtension implements AfterEachCallback, ParameterResolver {
private static final Map<String, WireMockServer> serverRepository = new HashMap<>();
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return isWireMockPortLockingParameter(parameterContext);
}
// Executed when test starts
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
if (!isWireMockPortLockingParameter(parameterContext)) {
return null;
}
String portName = resolvePortName(parameterContext);
int port = PortPool.INSTANCE.releasePort(portName);
WireMockServer server = new WireMockServer(port);
server.start();
serverRepository.put(portName, server);
return server;
}
// Executed after test ends
@Override
public void afterEach(ExtensionContext context) {
serverRepository.forEach((portName, server) -> {
if (server.isRunning()) {
server.stop();
PortPool.INSTANCE.lockPort(portName);
}
});
}
private boolean isWireMockPortLockingParameter(ParameterContext parameterContext) {
return parameterContext.getParameter().getType().equals(WireMockServer.class) &&
PortPool.INSTANCE.hasPort(resolvePortName(parameterContext));
}
private String resolvePortName(ParameterContext parameterContext) {
return parameterContext.getParameter().getName();
}
}
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
If a test contains single WireMockServer
, it can be configured for static use. In this example, we can call WireMock.configureFor(new WireMock(backendService))
at the start of the test.
Similar approach may be used directly in the test extension. For example if only one or first server is started it will be configured and other servers has to be used explicitly. On the other hand, sunch magic behaviour can be confusing for users.
@SpringBootTest
@ContextConfiguration(initializers = PortLockingContextInitializer.class)
@ExtendWith(WireMockPortLockingExtension.class)
class BackendServiceClientTest {
@Autowired
private BackendServiceClient client;
@Test
void callsBackendService(WireMockServer backendService) {
backendService.givenThat(get(urlPathEqualTo("/some-path"))
.willReturn(aResponse()
.withStatus(200)));
client.call();
backendService.verify(getRequestedFor(urlPathEqualTo("/some-path")));
}
}