The Problem
Testcontainers is an amazing library for integration testing with Docker containers. However, there was
a long-standing issue (#554) - the getMappedPort() method only supported TCP ports, not UDP.
This was problematic for developers testing applications that use UDP protocols like DNS servers, game servers, VoIP applications, or any real-time streaming services.
Understanding Docker Port Mapping
Docker supports both TCP and UDP port mappings. When you run a container, you can expose ports like:
# TCP port (default)
docker run -p 8080:80 nginx
# UDP port
docker run -p 53:53/udp dns-server
# Both TCP and UDP on same port
docker run -p 53:53/tcp -p 53:53/udp dns-server
The Docker API returns port bindings with protocol information, but Testcontainers was ignoring the protocol and assuming everything was TCP.
The Solution
I implemented a new overloaded method that accepts the protocol:
API Design
// Existing method (TCP only)
Integer getMappedPort(int originalPort);
// New method with protocol support
Integer getMappedPort(int originalPort, InternetProtocol protocol);
// New method to expose UDP ports
void addExposedPort(int port, InternetProtocol protocol);
Implementation in ContainerState.java
default Integer getMappedPort(int originalPort, InternetProtocol protocol) {
Preconditions.checkState(
isRunning(),
"Container must be running to get mapped port"
);
Ports.Binding[] bindings = getContainerInfo()
.getNetworkSettings()
.getPorts()
.getBindings()
.get(new ExposedPort(originalPort, protocol));
if (bindings == null || bindings.length == 0) {
throw new IllegalArgumentException(
"No mapped port for " + originalPort + "/" + protocol
);
}
return Integer.valueOf(bindings[0].getHostPortSpec());
}
Implementation in GenericContainer.java
public void addExposedPort(int port, InternetProtocol protocol) {
exposedPorts.add(new ExposedPort(port, protocol));
}
Testing the Implementation
I wrote comprehensive unit tests using Mockito to verify the behavior:
@Test
void shouldReturnMappedUdpPort() {
// Setup mock container with UDP port binding
when(containerInfo.getNetworkSettings()
.getPorts()
.getBindings()
.get(new ExposedPort(53, InternetProtocol.UDP)))
.thenReturn(new Ports.Binding[]{
new Ports.Binding("0.0.0.0", "32853")
});
// Verify UDP port mapping works
Integer mappedPort = container.getMappedPort(53, InternetProtocol.UDP);
assertThat(mappedPort).isEqualTo(32853);
}
@Test
void shouldThrowWhenUdpPortNotMapped() {
when(containerInfo.getNetworkSettings()
.getPorts()
.getBindings()
.get(any()))
.thenReturn(null);
assertThatThrownBy(() ->
container.getMappedPort(53, InternetProtocol.UDP)
).isInstanceOf(IllegalArgumentException.class);
}
Backward Compatibility
The existing getMappedPort(int) method continues to work as before, defaulting to TCP.
This ensures that existing code doesn't break:
// Still works - assumes TCP
container.getMappedPort(8080);
// New - explicitly specify protocol
container.getMappedPort(53, InternetProtocol.UDP);
Key Learnings
- Docker internals: Understanding how Docker handles port bindings at the API level
- API design: Adding new functionality without breaking existing users
- Test isolation: Using mocks for unit tests when Docker isn't available
- Protocol handling: TCP vs UDP networking differences