Repository navigation
Ports and Lifecycle
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.
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() 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.
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 throwsOperationCanceledException(the returned task is cancelled) if its token is already cancelled. -
StopAsync()does whatStop()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 andRequestReceivedevent handlers) until it is started again. It is safe to call repeatedly, on a server that was never started, and together withStop(). -
DisposeAsync()isStopAsync()followed byDispose().
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.
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.
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