Skip to content

Ports and Lifecycle

github-actions[bot] edited this page Oct 6, 2026 · 4 revisions

Ports and Lifecycle

Use port 0

A hard-coded port fails when another test or program is already using it, which happens often when tests run in parallel. Pass 0 and the operating system picks a free port. Read it from server.Port after Start():

using var server = new MockServer(new TcpServer(0));
server.Start();

Assert.InRange(server.Port, 1, 65535);

Then give server.Port to your client, for example through its configuration:

var client = new QuoteClient(server.Port, TimeSpan.FromSeconds(1));

For UdpServer the port is known as soon as the server is created, because UDP binds right away.

A UnixSocketServer has no port: it listens on a socket file, listener.Path, a unique one in the temp directory by default (see Unix domain sockets). A server from a configuration file uses port 0 when the file has none, so MockServer.FromFile(...) gives a free port as well.

Many servers side by side

Each server gets its own free port, so tests that run in parallel never collide:

var servers = Enumerable.Range(0, 5).Select(_ => new MockServer(new TcpServer(0))).ToList();
try
{
    foreach (var (server, index) in servers.Select((s, i) => (s, i)))
    {
        server.Mock.Send("who").Receive($"server {index}");
        server.Start();
    }

    for (var i = 0; i < servers.Count; i++)
    {
        using var client = await TcpTestClient.ConnectAsync(servers[i].Port);
        Assert.Equal($"server {i}", await client.SendAndReceiveAsync("who"));
    }
}
finally
{
    servers.ForEach(s => s.Dispose());
}

Start, stop and restart

Start() and Stop() are safe to call more than once:

using var server = new MockServer(new TcpServer(0));

server.Start();
server.Start();   // no-op
server.Stop();
server.Stop();    // no-op

Assert.False(server.Active);

A stopped server can be started again. A server created with port 0 keeps the port it was given, so a client can reconnect to the same address. This is useful for testing reconnect logic:

using var server = new MockServer(new TcpServer(0));
server.Mock.Send("ping").Receive("pong");

server.Start();
var port = server.Port;
server.Stop();
server.Start();

Assert.Equal(port, server.Port);

What Stop() does:

  • Stops accepting new connections and closes every open connection.
  • Cancels responses that are still waiting on a delay (After).
  • Keeps the configured responses and recorded requests.

Async lifecycle

StartAsync(), StopAsync() and DisposeAsync() are the awaitable forms of Start(), Stop() and Dispose():

await using var server = new MockServer(new TcpServer(0));
server.Mock.Send("PING").Receive("PONG");
await server.StartAsync();

using var client = await TcpTestClient.ConnectAsync(server.Port);
Assert.Equal("PONG", await client.SendAndReceiveAsync("PING"));
  • StartAsync() completes once the server is listening, so a client can connect right away. It throws OperationCanceledException (the returned task is cancelled) if its token is already cancelled.
  • StopAsync() does what Stop() does, then waits until the server's background work has ended: the listen loop, every request, delayed response and greeting in flight, and connections that were still being opened. After it completes, the server calls no more callbacks of yours (Log, Receive(...) functions, matcher predicates, connection and RequestReceived event handlers) until it is started again. It is safe to call repeatedly, on a server that was never started, and together with Stop().
  • DisposeAsync() is StopAsync() followed by Dispose().

Prefer await using in tests: when the test method returns, no callback or log line runs any more, so a Log that writes to the test output never fails with "no currently active test". With a plain using, a delayed response may still finish after the test.

Do not await StopAsync() or DisposeAsync() from inside one of the server's own callbacks (a Receive(...) function, a predicate, Log or a connection event handler): it would wait for itself. A custom IListener gets the guarantee only for the server's own tasks, not for work the listener runs itself.

Configure before or after Start

Responses can be added at any time, from any thread:

using var server = new MockServer(new TcpServer(0));
server.Start();

server.Mock.Send("late").Receive("still works");

using var client = await TcpTestClient.ConnectAsync(server.Port);
Assert.Equal("still works", await client.SendAndReceiveAsync("late"));

TCP options (KeepAlive, Framing) should be set before Start(). A connection uses the framing that was set when it opened.

Dispose

Always dispose the server, most simply with using var server = .... Disposing stops it and releases the socket, so the port is free for the next test.

To share one server across the tests of a class, see Recipes.

Runnable code: PortAndLifecycleSamples.cs

Clone this wiki locally