Repository navigation
Stateful Scenarios
Real servers remember things. LIST only works after LOGIN, a job reports RUNNING until it is DONE, and a
server in maintenance answers everything with an error. Sequences change the response to one
request; scenario states change which rules apply to every request.
Every server starts in the state "initial" (RequestHandler.InitialState).
-
.GoTo("state")after a response moves the scenario to that state once the response is used. -
InState("state")beforeSend(...)makes a rule apply only in that state. - Rules without
InStateapply in every state.
server.Mock.Send("LOGIN bob").Receive("OK").GoTo("loggedIn");
server.Mock.InState("loggedIn").Send("LIST").Receive("a,b,c");
server.Mock.InState("loggedIn").Send("LOGOUT").Receive("BYE").GoTo(RequestHandler.InitialState);
server.Mock.Send("LIST").Receive("ERR not logged in");
server.Start();
using var client = await TcpTestClient.ConnectAsync(server.Port);
Assert.Equal("ERR not logged in", await client.SendAndReceiveAsync("LIST"));
Assert.Equal("OK", await client.SendAndReceiveAsync("LOGIN bob"));
Assert.Equal("a,b,c", await client.SendAndReceiveAsync("LIST"));
Assert.Equal("BYE", await client.SendAndReceiveAsync("LOGOUT"));
Assert.Equal("ERR not logged in", await client.SendAndReceiveAsync("LIST"));InState(...) works with every way of matching a request: Send(text), Send(bytes),
Send(Regex), SendMatching(...), SendMatchingBytes(...) and Send("") for any request in that state.
The usual precedence applies: exact requests, then patterns and predicates,
then "any request". At each of these levels, a rule for the current state wins over a rule without a state.
So an exact Send("PING") without a state still wins over InState("busy").Send(new Regex(".*")).
Each response in a sequence can move the scenario, so a sequence can end in a new state:
server.Mock.Send("START").Receive("STARTED").GoTo("running");
server.Mock.InState("running").Send("STATUS")
.Receive("RUNNING 10%")
.Then("RUNNING 60%")
.Then("DONE").GoTo("finished");
server.Mock.InState("finished").Send("STATUS").Receive("DONE");
server.Mock.Send("STATUS").Receive("IDLE");The state changes when the server picks the response, before any .After(delay), so the next request already sees
the new state.
Assert.Equal("initial", server.Mock.State);
server.Mock.State = "maintenance"; // start a test in a given state
server.Should().BeInState("maintenance");server.Mock.Reset() removes the rules and moves the scenario back to "initial".
By default there is one state for the whole server, so a GoTo(...) on one connection affects every client.
For protocols where every connection is its own session, keep a state per connection:
server.Mock.StateScope = StateScope.Connection;
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.Start();
using var bob = await TcpTestClient.ConnectAsync(server.Port);
using var stranger = await TcpTestClient.ConnectAsync(server.Port);
await bob.SendAndReceiveAsync("LOGIN bob");
Assert.Equal("a,b,c", await bob.SendAndReceiveAsync("LIST"));
Assert.Equal("ERR not logged in", await stranger.SendAndReceiveAsync("LIST"));
Assert.Equal("loggedIn", server.Connections[0].State);
Assert.Equal("initial", server.Connections[1].State);Every new connection starts in "initial". For UDP, every client address has its own state. Requests passed to
server.Mock.Match(...) directly use the server-wide server.Mock.State.
A greeting can start the session:
server.Mock.StateScope = StateScope.Connection;
server.Mock.OnConnect().Receive("+OK POP3 ready").GoTo("authorization");
server.Mock.InState("authorization").Send("USER bob").Receive("+OK");The log shows the state each request was matched in and every state change:
[Rony 10:15:02.104] #1 received "LOGIN bob" (matched "LOGIN bob")
[Rony 10:15:02.105] #1 state "initial" -> "loggedIn"
[Rony 10:15:02.118] #1 received "LIST" (matched "LIST" in state "loggedIn")
[Rony 10:15:02.131] #1 received "HELP" (unmatched, state "loggedIn")
Runnable code: ScenarioSamples.cs