The library implements recorder and player of HTTP requests. When recording, a request to a remote service and its live response are serialized to JSON and saved to a file (cassette), so that next time the same request is made through the player it can be served with the previously recorded response. This scenario is common for integration tests running HTTP requests in isolation.
PM> Install-Package HttpMockPlayer
The library is built on top of .NET Standard 2.0.
The following example uses NUnit testing framework together with the HttpMockPlayer library to test a remote API:
...
MyAPIclient client;
Player player;
Cassette cassette;
[OneTimeSetUp]
public void OneTimeSetUp()
{
var baseAddress = new Uri("http://localhost:5555/");
var remoteAddress = new Uri("https://api.myserver.com");
client = new MyAPIclient(baseAddress);
player = new Player(baseAddress, remoteAddress);
player.Start();
cassette = new Cassette("/path/to/mock/api-calls.json");
player.Load(cassette);
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
player.Close();
client.Close();
}
[SetUp]
public void SetUp()
{
var record = TestContext.CurrentContext.Test.Name;
if (cassette.Contains(record))
{
player.Play(record);
}
else
{
player.Record(record);
}
}
[TearDown]
public void TearDown()
{
player.Stop();
}
[Test]
public void GetStuff_ReturnsStuff()
{
var res = client.GetStuff();
Assert.IsNotNull(res);
...
}
[Test]
public void CreateStuff_Unauthorized_Throws()
{
...
Assert.Throws<HttpRequestException>(() => client.CreateStuff(param1, param2));
}
Once run, this code generates a JSON file (api-calls.json
as specified in the cassette constructor):
[
{
"name": "GetStuff_ReturnsStuff",
"requests": [
{
"request": {
"method": "GET",
"uri": "https://api.myserver.com/stuff",
"headers": {
"Connection": "Keep-Alive",
"Host": "api.myserver.com",
"User-Agent": "MyAPIclient/1.0"
}
},
"response": {
"statusCode": 200,
"statusDescription": "OK",
"content": "{\"id\":\"a77a6649-1d41-4d80-a763-494640b99a4b\",
\"items\":[]}",
"headers": {
"Status": "200 OK",
"X-Served-By": "a123456f3b2fa272558fa6dc951018ad",
"Content-Length": "56",
"Cache-Control": "public, max-age=60, s-maxage=60",
"Content-Type": "application/json; charset=utf-8",
"Date": "Tue, 28 Jun 2016 20:06:06 GMT",
"Last-Modified": "Tue, 28 Jun 2016 00:40:50 GMT",
"Server": "myserver.com"
}
}
}
]
},
{
"name": "CreateStuff_Unauthorized_Throws",
"requests": [
{
"request": {
"method": "POST",
"uri": "https://api.myserver.com/stuff",
"content": "{\"id\":\"186b0d73-4ab6-4726-a963-85996944b6b4\",
\"items\":[]}",
"headers": {
"Content-Length": "56",
"Content-Type": "application/json; charset=utf-8",
"Expect": "100-continue",
"Host": "api.myserver.com",
"User-Agent": "MyAPIclient/1.0"
}
},
"response": {
"statusCode": 401,
"statusDescription": "Unauthorized",
"content": "{\"message\":\"Requires authentication\"}",
"headers": {
"Status": "401 Unauthorized",
"Content-Length": "37",
"Content-Type": "application/json; charset=utf-8",
"Date": "Tue, 28 Jun 2016 20:06:07 GMT",
"Server": "myserver.com"
}
}
}
]
}
]
The Player
object is initialized with the (local) base address, where it listens for client requests, and an address of a remote service, to which the requests are redirected when recording. Note that the player serves as a forward proxy and does not intercept HTTP requests, so the client should send its requests to the player's base address in order for them to be recorded or replayed. Remote address can be changed while the player runs, which makes it possible to test distributed web services.
Check the HttpMockPlayer.Samples project for more examples.
After initialization the player should be started, so that it can accept incoming requests. It is also required to load a cassette - JSON file where requests will be read from or written to. If the file doesn't exist, it will be created automatically as soon as recording is stopped.
Once started and loaded, the player can play or record a series of requests (a record). If it is already in operation, it won't switch to another record until stopped.
var record = "CreateNewReport";
if (cassette.Contains(record))
{
player.Play(record);
}
else
{
player.Record(record);
}
...
// authentication request
// request to fetch additional data
// request to post a new report
player.Stop();
When all done the player should be shut down:
player.Close();
A HTTP request is recorded as JSON object which stores its HTTP method, resource URI, body data and headers:
"request": {
"method": "POST",
"uri": "https://api.test.com/entities/1",
"content": "entity1",
"headers": {
"Connection": "Keep-Alive",
"Content-Length": "7",
"Cookie": "cookie1=value1; cookie2=value2",
"Host": "api.github.com",
"User-Agent": "SampleGithubClient/1.0"
}
}
For a HTTP response its status code, status description, body data and headers are saved:
"response": {
"statusCode": 200,
"statusDescription": "Ok",
"content": "<!DOCTYPE html><html></html>",
"headers": {
"Connection": "keep-alive",
"Content-Length": "28",
"Cache-Control": "no-cache,no-store,max-age=0,must-revalidate",
"Content-Type": "text/html; charset=UTF-8",
"Date": "Thu, 07 Jul 2016 01:06:49 GMT",
"Expires": "Thu, 07 Jul 2016 01:06:50 GMT",
"Last-Modified": "Thu, 07 Jul 2016 01:06:50 GMT",
"Set-Cookie": "yp=1470445610.ygu.1; Expires=Sun, 05-Jul-2026 01:06:49 GMT; Domain=.ya.ru; Path=/",
"Server": "nginx",
"X-Frame-Options": "DENY"
}
}
In certain cases Player
object throws custom exceptions:
CassetteException
if a cassette cannot be read or saved;PlayerStateException
if player is not in a valid state to start an operation (for instance, whenPlay()
is not followed byStop()
, but instead anotherPlay()
is called)
If an error occurs while processing a client request, the player wraps it into the response message and sets the status code to 454 (request mismatch), 551 (play exception), 552 (record exception) or 550 (general player exception).