A mock server for testing .NET code that talks over the network. Start a real TCP, TCP + SSL/TLS, UDP or Unix socket server inside your test, tell it how to answer, point your client at it, and then check what your client sent.
using var server = new MockServer(new TcpServer(0)); // 0 = any free port
server.Mock.Send("PING").Receive("PONG");
server.Start();
// ... run the code under test against 127.0.0.1:server.Port ...
server.Should().HaveReceived("PING", Times.Once());📖 Full documentation, with an example for every feature, is in the wiki.
- Real sockets. Your client code runs unchanged: no interfaces to extract, no fake streams. Includes TLS with a built-in test certificate and mutual TLS, IPv6, dual-stack and Unix domain sockets. → Servers, SSL and TLS
- Free ports and a clean lifecycle. Port
0means tests never fight over ports, even in parallel; start and stop synchronously or withawait using. → Ports and Lifecycle - Any protocol. Text or binary; persistent connections; delimited, length-prefixed, fixed-length, STX/ETX or custom messages. → Connections and Framing
- Flexible matching. Exact requests, regular expressions with capture groups, JSON fields, predicates, a default response and a handler for unmatched requests. → Request Matching
- Scripted responses. Fixed, computed from the request, or a different one each time. → Configuring Responses, Response Sequences
- Server-initiated messages. Greetings on connect, pushed messages and broadcasts; connection list and events. → Connections and Push
- Stateful scenarios. "
LISTonly works afterLOGIN", for the whole server or per connection. → Stateful Scenarios - Failure testing. Delays, chunked and throttled responses, dropped and reset connections, truncated or corrupted responses, refused connections, failing TLS handshakes, silence and flaky servers. → Simulating Failures
- Assertions on your client. Fluent
server.Should()andconnection.Should()assertions withTimes, order, strict or fail-fast mode, connection checks, TLS details (protocol, server name, client certificate), and waiting for requests and connections without sleeps; aRequestReceivedevent,ToJson()for a journal and caps on the kept requests and connection records. → Verifying Requests, Waiting for Requests - Record and replay. Record the conversation with a real server through a proxy, save it as an editable JSON file and replay it as a mock server. → Record and Replay
- Configuration files. Describe the server and its rules in a JSON file and load it with one call, with optional address and port overrides, validation without a socket and reloading the rules of a running server. → Configuration Files
- Standalone server. The
ronycommand-line tool (also published as the Docker imageghcr.io/archofthings/rony) runs a configuration file, records a real server and replays the recording, with an optional journal of every received request, a control port to query the requests and the state, reloading on file changes (--watch), a Testcontainers module (Rony.Net.Testcontainers) to start it from a .NET test, and no .NET test code: a stand-in for a dependency during development, a mock for teams and CI jobs that do not use .NET. → Standalone Server - Easy debugging. A log of every connection, request, matched rule, response and error. → Logging and Diagnostics
- Works everywhere. .NET Core 3.x and every later .NET, with xUnit v2 or v3, NUnit or MSTest (with optional base classes), on Windows, Linux and macOS. → Test Framework Integration
dotnet add package Rony.NetOr in the Package Manager Console: Install-Package Rony.Net.
Optional, for less setup code: Rony.Net.Xunit (xUnit v2), Rony.Net.Xunit.v3 (xUnit v3), Rony.Net.NUnit or Rony.Net.MSTest
(Test Framework Integration).
To run a mock server without code: the Rony.Net.Cli tool (Standalone Server).
To start that tool in a Docker container from a .NET test: Rony.Net.Testcontainers (Testcontainers module).
using Rony; // GetBytes() / GetString() helpers
using Rony.Listeners; // TcpServer, TcpServerSsl, UdpServer, MessageFraming
using Rony.Net; // MockServer, Times
[Fact]
public async Task Client_gets_pong()
{
using var server = new MockServer(new TcpServer(0));
server.Mock.Send("PING").Receive("PONG");
server.Start();
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, server.Port);
var stream = client.GetStream();
await stream.WriteAsync("PING".GetBytes());
var buffer = new byte[1024];
var read = await stream.ReadAsync(buffer);
Assert.Equal("PONG", buffer[..read].GetString());
server.Should().HaveReceived("PING", Times.Once());
}More in Getting Started.
new MockServer(new TcpServer(0)); // TCP on 127.0.0.1
new MockServer(new TcpServerSsl(0, certificate, SslProtocols.None)); // TCP + SSL/TLS
new MockServer(new UdpServer("127.0.0.1", 0)); // UDP
new MockServer(new UnixSocketServer()); // Unix domain socket; the file is listener.Path
new TcpServer(IPAddress.IPv6Any, 0) { DualMode = true }; // IPv4 and IPv6 on one port (UdpServer: new UdpServer(endPoint, dualMode: true))
using var certificate = TestCertificate.CreateSelfSigned(); // self-signed certificate for TLS tests, nothing is added to a store
new TcpServerSsl(0, certificate, SslProtocols.Tls12) { RequireClientCertificate = true }; // mutual TLS
connection.Should().HaveUsedTls(SslProtocols.Tls12).And.HavePresentedClientCertificate(); // TLS details of a connection
await using var server = new MockServer(new TcpServer(0)); // async lifecycle: also StartAsync(), StopAsync()TCP connections stay open, so a client can send many requests over one connection. Each connection is handled
independently, and responses keep their order. await using and StopAsync() wait for the server's background work,
so no callback or log line runs after the test.
Details: Servers · SSL and TLS (test certificate, mutual TLS, TLS assertions and failing the handshake) ·
Ports and Lifecycle
TCP doesn't keep message boundaries. Tell the server where messages end, and it splits requests and frames responses for you:
new TcpServer(0) { Framing = MessageFraming.Delimiter("\r\n") }; // line-based protocols
new TcpServer(0) { Framing = MessageFraming.LengthPrefix(2) }; // binary, length-prefixed
new TcpServer(0) { Framing = MessageFraming.LengthPrefix(4, bigEndian: false) }; // little-endian
new TcpServer(0) { Framing = MessageFraming.LengthPrefix(2, bigEndian: true, includesPrefix: true) }; // length counts itself
new TcpServer(0) { Framing = MessageFraming.FixedLength(8) }; // fixed-size records
new TcpServer(0) { Framing = MessageFraming.StxEtx }; // 0x02 ... 0x03; StartEnd(start, end) for other bytes
new TcpServer(0) { KeepAlive = false }; // close after every responseDetails, and custom framing: Connections and Framing
server.Mock.Send("version").Receive("1.0.0"); // text
server.Mock.Send(new byte[] { 0x01, 0x02 }).Receive(new byte[] { 0x03 }); // bytes
server.Mock.Send("hello").Receive(text => text.ToUpper()); // computed
server.Mock.Send("").Receive("ERROR unknown command"); // any other requestDetails: Configuring Responses
server.Mock.Send(new Regex(@"^LOGIN \w+$")).Receive("WELCOME");
server.Mock.SendMatching(text => text.StartsWith("GET ")).Receive("200 OK");
server.Mock.SendMatchingBytes(bytes => bytes[0] == 0x02).Receive(new byte[] { 0x06 });
server.Mock.Send(new Regex(@"^HELLO (\w+)$")).ReceiveMatch(m => $"HI {m.Groups[1].Value}");
server.Mock.SendJson(j => j["type"].AsString() == "login").Receive("{\"ok\":true}");An exact request wins over patterns and predicates, which win over the Send("") default.
Details: Request Matching
server.Mock.Send("status").Receive("busy").Then("busy").Then("ready"); // busy, busy, ready, ready, ...Details: Response Sequences
server.Mock.Send("report").Receive("done").After(TimeSpan.FromSeconds(2)); // slow
server.Mock.Send("pay").Disconnect().Then("PAID"); // drop once, then succeed
server.Mock.Send("ping").NoReply(); // never answer
server.Mock.Send("QUIT").Receive("BYE").AndDisconnect(); // reply, then hang up
server.Mock.Send("X").ResetConnection(); // TCP reset instead of a clean close
server.Mock.Send("X").Receive("HELLO WORLD").Truncated(5).AndDisconnect(); // only the first 5 bytes
server.Mock.Send("X").Receive("HELLO").Corrupted(bytes => { bytes[0] ^= 0xFF; return bytes; });
server.Mock.Send("GET").Receive(body).InChunks(16, TimeSpan.FromMilliseconds(50)); // piece by piece
server.Mock.Send("GET").Receive(body).Throttled(bytesPerSecond: 1024); // slow link
server.RefuseConnections(); // "connection refused" until AcceptConnections()
new TcpServerSsl(0, certificate, SslProtocols.Tls12) { FailHandshake = true }; // every TLS handshake failsDetails: Simulating Failures
server.Mock.OnUnmatched().Receive(text => $"ERR unknown command '{text}'"); // answer, keep the connection
server.Mock.FailOnUnmatched = true; // fail Verify/WaitFor right awayDetails: Request Matching
server.Mock.Send("LOGIN bob").Receive("OK").GoTo("loggedIn");
server.Mock.InState("loggedIn").Send("LIST").Receive("a,b,c");
server.Mock.Send("LIST").Receive("ERR not logged in");
server.Mock.StateScope = StateScope.Connection; // optional: a session per connectionDetails: Stateful Scenarios
using var proxy = new RecordingProxy("real.host", 5000) { Framing = MessageFraming.Delimiter("\n") };
proxy.Start(); // point your client at proxy.Port
await proxy.WaitForConnectionsClosedAsync();
proxy.Recording.Save("login.rony.json"); // editable JSON
server.Replay(Recording.Load("login.rony.json")); // later, in tests: rules from the recordingDetails: Record and Replay
// mock.json: { "version": 1, "rules": [ { "request": "PING", "reply": "PONG" } ] }
using var server = MockServer.FromFile("mock.json"); // listener and rules from the file
server.Start();
using var other = MockServer.FromFile("mock.json", new ConfigurationOverrides { Port = 0 }); // another port than the file says
MockServer.ValidateFile("mock.json"); // check a file without opening a socket
server.ReloadFile("mock.json"); // new rules for the running server; connections and state are keptDetails: Configuration Files
Use the mock outside of .NET tests: as a stand-in for a service during local development, for a team or a CI job that does not use .NET, or in a container.
dotnet tool install --global Rony.Net.Cli
rony run mock.json [--port 0] [--address 0.0.0.0] # serve a configuration file (Ctrl+C stops it)
rony run mock.json --journal requests.jsonl # append every received request to a file, one JSON line each
rony run mock.json --control 0 # a loopback control port (--control-address <ip> changes the address): ask for the received requests, clear them, read or set the state
rony run mock.json --watch # reload the rules whenever the file changes (a broken file keeps the old rules)
rony validate mock.json # check a configuration file, for example in CI
rony record --target api.test:5000 --out login.json # record a real server through a proxy
rony replay login.json # serve the recording
docker run --rm -p 127.0.0.1:4000:4000 -v "$PWD:/config" ghcr.io/archofthings/rony # the same as a Docker image (linux/amd64 and arm64; also on Docker Hub as mojihub/rony)await using var rony = new RonyBuilder().WithConfigurationFile("mocks/shop.json").Build(); // Rony.Net.Testcontainers: the image in a container
await rony.StartAsync(); // rony.Hostname and rony.Port for the system under test
var requests = await rony.GetReceivedRequestsAsync(); // also ClearReceivedRequestsAsync, GetStateAsync, SetStateAsyncDetails and walk-throughs (development, CI, Docker Compose, TLS, record and replay, Testcontainers): Standalone Server
server.Mock.OnConnect().Receive("220 mail.test ready\r\n"); // greet every client first
await server.Connections[0].SendAsync("NOTIFY price-changed"); // push to one client
await server.BroadcastAsync("SHUTDOWN in 5 minutes"); // or to all of them
server.Should().HaveAcceptedConnections(Times.Once()); // the client reused its connection
await server.Connections[0].WaitForCloseAsync(); // and closed it
await server.WaitForAllConnectionsClosedAsync(); // or wait for all of them
server.Should().HaveNoOpenConnections();Details: Connections and Push
server.Should().HaveReceived("LIST", Times.Exactly(2))
.And.HaveReceived(r => r.BodyString.StartsWith("LOGIN"), Times.Once())
.And.NotHaveReceived("DELETE")
.And.HaveReceivedInOrder("LOGIN", "LIST", "QUIT") // order (others may be in between)
.And.HaveNoUnmatchedRequests(); // strict mode
await server.Mock.WaitForRequestAsync("HEARTBEAT"); // instead of Thread.Sleep
var requests = server.ReceivedRequests; // body, sender, time, matched
server.Mock.MaxReceivedRequests = 1000; // keep only the last 1000 (MaxConnectionRecords: connections)
server.RequestReceived += (sender, request) => journal.WriteLine(request.ToJson()); // one JSON line per request
connection.Should().HaveReceived("LOGIN bob", Times.Once()) // only what one connection sent
.And.HaveReceivedInOrder("LOGIN bob", "LIST")
.And.BeClosed();A failed check lists every request the server received. The same checks are also available as
server.Mock.Verify(...) methods.
Details: Verifying Requests · Waiting for Requests
server.Log = output.WriteLine; // xUnit's ITestOutputHelper, Console.WriteLine, ...[Rony 10:15:02.097] #1 connected from 127.0.0.1:50124
[Rony 10:15:02.102] #1 received "PING" (matched "PING")
[Rony 10:15:02.103] #1 sent "PONG"
Errors that are otherwise silent, such as an exception in a Receive(...) function or a failed TLS handshake, are logged too.
Details: Logging and Diagnostics
public class PingTests : MockServerTest // Rony.Net.Xunit or Rony.Net.Xunit.v3; also Rony.Net.NUnit and Rony.Net.MSTest
{
public PingTests(ITestOutputHelper output) : base(output) { }
[Fact]
public async Task Client_gets_pong()
{
Server.Mock.Send("PING").Receive("PONG"); // a started server per test, logging to the test output
// ...
}
}Details: Test Framework Integration
- Recipes: testing a real client class with retries and timeouts; xUnit, NUnit and MSTest setup.
- Custom Listeners: mock over your own transport, or with no network at all; optional interfaces add connections and failure simulation.
- API Reference · Troubleshooting · Known Issues
- Runnable samples: every wiki example as a passing test.
- The
ronytool and servers from a configuration file are for development and test networks: no connection or idle limits, and the control endpoint has no authentication. The tool keeps the last 10000 requests and connection records (--keep); a server in code keeps all unless you setMaxReceivedRequests/MaxConnectionRecords. - Mutual TLS accepts any presented client certificate unless you set
ClientCertificateValidator; when a rejected client notices depends on the OS. - Replay is order-dependent, does not replay recorded delays and has no UDP; recordings and logs contain everything on the wire, credentials included.
- Unix domain sockets have no TLS and no RST; in a Docker container the configuration must listen on
0.0.0.0.
Details and the full list: Known Issues and Limitations
1.0 keeps TCP connections open after a response. If your client reads until the server closes the connection, set
KeepAlive = false. See Upgrading to 1.0 and the
changelog.
While working on Cimon.Net, I couldn't find a library that mocks sockets. Faking sockets inside the project didn't really solve the problem, so I wrote this library and used it in Cimon.Net.
You need the .NET 8 SDK or later.
dotnet build
dotnet testThe tests need no setup: SSL/TLS tests create their certificate at runtime. CI runs everything, including the samples, on Linux and Windows.
The wiki source lives in docs/wiki and is published automatically.