Repository navigation
Record and Replay
Instead of writing every rule by hand, record a conversation with the real server once and replay it in your tests.
A RecordingProxy sits between your client and the real server and records what goes through; MockServer.Replay
turns the recording into ordinary rules.
// 1. Record against the real server
using var proxy = new RecordingProxy("real.host", 5000) { Framing = MessageFraming.Delimiter("\n") };
proxy.Start();
// ... point the client at proxy.Port and run it ...
await proxy.WaitForConnectionsClosedAsync(); // the recording is complete
proxy.Recording.Save("login.rony.json");The proxy is a plain TCP relay with its own sockets, listening on 127.0.0.1 and a free port by default
(proxy.Port after Start()). Bytes are forwarded unchanged and at once; the proxy only watches. Several connections
can go through it at the same time.
-
Framing.
Framingsplits both directions into recorded messages, exactly as aTcpServerdoes. Use the framing of the protocol to get one recorded message per protocol message. The default (MessageFraming.None) records every read as one message, which depends on how the bytes happen to arrive. Set it beforeStart(). If the framing throws, or more than 16 MiB arrive without a complete message, the proxy records the pending bytes as one raw message and the rest of that direction unframed; the bytes are still relayed. -
Waiting.
WaitForConnectionsClosedAsync()completes once at least one connection went through the proxy and all of them have ended (5 seconds by default, thenTimeoutException), so you can save a complete recording without sleeping.proxy.Recordingis always the same object, filled while traffic flows, and can be read or saved at any time. - Closing. When one side closes or resets the connection, the proxy records who did it first and closes the other side. A side that only finishes sending (a half-close) is passed on as a half-close on plain TCP, so the reply still comes back; with TLS the connection is closed instead.
-
Failures. If the real server cannot be reached, or a TLS handshake fails, the client is disconnected, the reason goes
to
proxy.Log(same idea asserver.Log) and the proxy keeps accepting. The connection is still in the recording, with a close by the server. -
Stopping.
Stop()/StopAsync()/Dispose()close every relayed connection and wait for them to end. Called from theLogcallback,Stop()andDispose()stop the proxy without waiting for the connections to end; after that,StopAsync()(orStop()from elsewhere) waits for them.
Certificate makes the proxy speak TLS to your client (like TcpServerSsl); TargetTls makes it speak TLS to the real
server, using the target host name for the handshake. TargetCertificateValidation is optional (default validation otherwise).
using var proxy = new RecordingProxy("localhost", real.Port)
{
Certificate = certificate,
TargetTls = true,
TargetCertificateValidation = (_, serverCertificate, _, _) => serverCertificate?.GetCertHashString() == certificate.GetCertHashString()
};The recording is JSON (version 1). Offsets (at) are milliseconds since the connection was accepted.
{
"version": 1,
"connections": [
{
"id": 1,
"messages": [
{ "from": "server", "at": 3, "text": "220 ready" },
{ "from": "client", "at": 12, "text": "LOGIN bob" },
{ "from": "server", "at": 15, "base64": "AAEC/w==" },
{ "from": "server", "at": 20, "closed": true }
]
}
]
}- A message body is written as
textwhen it is valid UTF-8 without control characters other than CR, LF and tab, and asbase64otherwise.closed: trueis the close event: that side closed (or reset) the connection first. It is always the last message of a connection. - The files are meant to be edited by hand: change a reply, delete an exchange, or add a message.
Recording.Parseignores unknown properties, so you can add notes such as"note": "...".atis optional. A message needs exactly one oftextorbase64(none for a close event). Anything else throws aFormatExceptionthat names the connection and message. -
new Recording()is empty, andRecording.Parse(json)/recording.ToJson()work with strings instead of files (Savewrites UTF-8 without a byte order mark, indented). The sample project replays this very recording.
// 2. Replay in tests
using var server = new MockServer(new TcpServer(0) { Framing = MessageFraming.Delimiter("\n") });
server.Replay(Recording.Load("login.rony.json"));
server.Start();Use the same framing on the server as the proxy used. Replay may be called before or after Start(). It adds rules to
server.Mock; it does not reset them. Calling it again for the same requests (or with another greeting) throws ArgumentException,
as for any request configured twice: call server.Mock.Reset() first to replace the rules. When Replay throws partway,
the rules added before the exception stay; call server.Mock.Reset() to start over.
How the recording becomes rules, connection by connection and message by message:
-
Greeting. The server messages before the first client message become
OnConnect(). A connection without a greeting is ignored. As for requests, identical greetings give one step; otherwise every greeting becomes a step, in recorded order, so the first connection gets the first greeting, the second the second, and so on. -
Exchanges. Each client message and the server messages that follow it become
Send(request).Receive(reply). The same request seen again, in this or a later connection, adds the next reply of a sequence in recorded order. If a request always got the same reply, one step is enough. -
No reply. A client message with no server message after it becomes
NoReply(), orDisconnect()if the server closed the connection right then. -
Server closes after its reply. The reply gets
AndDisconnect(). A close by the client is ignored. - Several server messages for one request are all sent, in order, as separate frames (TCP only).
- An empty request is skipped, because an empty
Send("")would match every request.
Recorded times are not replayed as delays: add .After(...) yourself if a test needs them.
The rony record and rony replay commands do the same without code, see Standalone Server.
- A recording and the proxy's log contain everything sent through the proxy, including credentials and tokens: review them before committing or sharing them.
- TCP and TLS only. The proxy does not relay UDP.
- Every request is matched on its own. A protocol whose reply depends on earlier requests replays correctly only when the client sends its requests in the recorded order. For anything smarter, edit the rules afterwards or use stateful scenarios.
- A recording made without the protocol's framing replays only if the client sends the bytes in the same pieces. Without framing, a reply may also be recorded ahead of the request it answers when the client pipelines data.
Runnable code: RecordAndReplaySamples.cs