Skip to main content

In the world of software development, it is important to ensure that our code works as expected in various environments. Testing plays a crucial role in ensuring that the code performs well. Containers have become increasingly popular as a means to easily manage and deploy applications. They are lightweight, portable, and can be spun up and torn down quickly.

Testcontainers is a popular library for writing integration tests that involve spinning up containers. The library helps to reduce the complexity of test setup by abstracting away the details of container creation and management.

One of the challenges with containerized tests is having containers access services running on the test host (outside of any container). This is particularly necessary whenever I benefit from containerizing my test driver, e.g. when driving a containerized browser against my system under test. However, this challenge has been simplified with the new port forwarding feature in Testcontainers for .NET.

To expose ports of services running on the test host to containers, we need to configure Testcontainers first. This configuration needs to be set up before configuring any container resources to ensure proper forwarding of traffic. Here’s how we can expose the host port 8080 using Testcontainers:

await TestcontainersSettings.ExposeHostPortsAsync(8080)
    .ConfigureAwait(false);

This line tells Testcontainers to set up an encrypted connection that forwards requests from a container to the test host. From the perspective of a container, the test host is available through the hostname host.testcontainers.internal. Ports are forwarded one-to-one.

Now that we have exposed the host port, we can connect to the service running on the test host from a container. Here’s an example of how we can use Netcat to connect to the test host:

_ = await _alpineContainer.ExecAsync(new[] { "nc", "host.testcontainers.internal", "8080" })
    .ConfigureAwait(false);

Let’s see port-forwarding in action

The following C# NUnit test class demonstrates a configuration that starts a TCP service on the test host that responds with the message Hello, World!. The test class uses the Testcontainers library to start an Alpine container and run a Netcat command connecting to the TCP service.

private const string HelloWorld = "Hello, World!";

private readonly TcpListener _tcpListener = new(new IPEndPoint(IPAddress.Any, 0));

public TestHostServiceTest()
{
    _tcpListener.Start();
}

private string Host => "host.testcontainers.internal";

private ushort Port => Convert.ToUInt16(((IPEndPoint)_tcpListener.LocalEndpoint).Port);

[OneTimeSetUp]
public async Task SetUp()
{
    await TestcontainersSettings.ExposeHostPortsAsync(Port)
        .ConfigureAwait(false);

    // Responds with "Hello, World!" to a new connection.
    _ = AcceptSocketAsync();
}

[Test]
public async Task TestHostServiceReturnsHelloWorld()
{
    // Given
    var curlContainer = new ContainerBuilder()
        .WithImage("alpine:3.17")
        .WithEntrypoint("nc")
        .WithCommand(Host, Port.ToString())
        .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil()))
        .Build();

    // When
    await curlContainer.StartAsync()
        .ConfigureAwait(false);

    var (stdout, _) = await curlContainer.GetLogsAsync(timestampsEnabled: false)
        .ConfigureAwait(false);

    // Then
    Assert.Equal(HelloWorld, stdout);
}

private async Task AcceptSocketAsync()
{
    var sendBytes = Encoding.Default.GetBytes(HelloWorld);

    using var socket = await _tcpListener.AcceptSocketAsync()
        .ConfigureAwait(false);

    _ = await socket.SendAsync(sendBytes, SocketFlags.None)
        .ConfigureAwait(false);
}

private sealed class WaitUntil : IWaitUntil
{
    public Task<bool> UntilAsync(IContainer container)
    {
        return Task.FromResult(TestcontainersStates.Exited.Equals(container.State));
    }
}

The [OneTimeSetUp] attribute is used to run the SetUp method once before any test in the class, which exposes the ephemeral port used by the TCP listener to Testcontainers. The actual test retrieves the response message from the container’s logs and asserts that it matches the expected message.

The port forwarding feature is not only supported by Testcontainers for .NET but is also supported by other language implementations such as Java, and Node and is, of course, available for Testcontainers Cloud users.

Conclusion

The new port forwarding feature in Testcontainers for .NET simplifies the configuration required to expose ports of services running on the test host to containers. This feature helps to reduce the complexity of test setups and allows developers to focus on writing tests that ensure their code works as expected in various environments.