Extends Verify to allow verification of Http bits.
See Milestones for release notes.
Entity Framework Extensions is a major sponsor and is proud to contribute to the development this project.
Call VerifierSettings.InitializePlugins() in a [ModuleInitializer].
public static class ModuleInitializer
{
[ModuleInitializer]
public static void Initialize() =>
VerifierSettings.InitializePlugins();
}Or, if order of plugins is important, use VerifyHttp.Initialize() in a [ModuleInitializer].
[Test]
public async Task ScrubHttpTextResponse()
{
using var client = new HttpClient();
using var result = await client.GetAsync("https://httpcan.org/html");
await Verify(result)
.ScrubHttpTextResponse(_ => _.Replace("Herman Melville - Moby-Dick", "New title"));
}Enable at any point in a test using VerifyTests.Recording.Start().
Includes converters for the following
HttpMethodUriHttpHeadersHttpContentHttpRequestMessageHttpResponseMessage
For example:
[Test]
public async Task HttpResponse()
{
using var client = new HttpClient();
var result = await client.GetAsync("https://httpcan.org/json");
await Verify(result);
}{
Status: 200 OK,
Headers: {
Access-Control-Allow-Credentials: true,
Alt-Svc: h3=":443",
cf-cache-status: DYNAMIC,
Connection: keep-alive,
Date: DateTime_1,
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800},
Server: cloudflare,
Vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers
},
Content: {
Headers: {
Content-Length: 274,
Content-Type: application/json
},
Value: {
slideshow: {
author: Yours Truly,
date: date of publication,
slides: [
{
title: Wake up to WonderWidgets!,
type: all
},
{
items: [
Why <em>WonderWidgets</em> are great,
Who <em>buys</em> WonderWidgets
],
title: Overview,
type: all
}
],
title: Sample Slide Show
}
}
}
}Headers are treated as properties, and hence can be ignored using IgnoreMember:
[Test]
public async Task IgnoreHeader()
{
using var client = new HttpClient();
using var result = await client.GetAsync("https://httpcan.org/get");
await Verify(result)
.IgnoreMembers(
"Server",
"Content-Length",
"Access-Control-Allow-Credentials");
}For code that does web calls via HttpClient, these calls can be recorded and verified.
Given a class that does some Http calls:
// Resolve a HttpClient. All http calls done at any
// resolved client will be added to `recording.Sends`
public class MyService(HttpClient client)
{
public Task MethodThatDoesHttp() =>
// Some code that does some http calls
client.GetAsync("https://httpcan.org/status/200");
}Http recording can be added to a IHttpClientBuilder:
var collection = new ServiceCollection();
collection.AddScoped<MyService>();
var httpBuilder = collection.AddHttpClient<MyService>();
// Adds a AddHttpClient and adds a RecordingHandler using AddHttpMessageHandler
var recording = httpBuilder.AddRecording();
await using var provider = collection.BuildServiceProvider();
var myService = provider.GetRequiredService<MyService>();
await myService.MethodThatDoesHttp();
await Verify(recording.Sends)
.IgnoreMember("Date");Http can also be added globally IHttpClientBuilder:
var collection = new ServiceCollection();
collection.AddScoped<MyService>();
// Adds a AddHttpClient and adds a RecordingHandler using AddHttpMessageHandler
var (builder, recording) = collection.AddRecordingHttpClient();
await using var provider = collection.BuildServiceProvider();
var myService = provider.GetRequiredService<MyService>();
await myService.MethodThatDoesHttp();
await Verify(recording.Sends)
.IgnoreMember("Date");[
{
RequestUri: https://httpcan.org/status/200,
RequestMethod: GET,
ResponseStatus: OK 200,
ResponseHeaders: {
Access-Control-Allow-Credentials: true,
Alt-Svc: h3=":443",
cf-cache-status: DYNAMIC,
Connection: keep-alive,
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800},
Server: cloudflare,
Vary: Origin|Access-Control-Request-Method|Access-Control-Request-Headers
},
ResponseContent: {"status":200}
}
]There a Pause/Resume semantics:
var collection = new ServiceCollection();
collection.AddScoped<MyService>();
var httpBuilder = collection.AddHttpClient<MyService>();
// Adds a AddHttpClient and adds a RecordingHandler using AddHttpMessageHandler
var recording = httpBuilder.AddRecording();
await using var provider = collection.BuildServiceProvider();
var myService = provider.GetRequiredService<MyService>();
// Recording is enabled by default. So Pause to stop recording
recording.Pause();
await myService.MethodThatDoesHttp();
// Resume recording
recording.Resume();
await myService.MethodThatDoesHttp();
await Verify(recording.Sends)
.ScrubInlineDateTimes("R");If the AddRecordingHttpClient helper method does not meet requirements, the RecordingHandler can be explicitly added:
var collection = new ServiceCollection();
var builder = collection.AddHttpClient("name");
// Change to not recording at startup
var recording = new RecordingHandler(recording: false);
builder.AddHttpMessageHandler(() => recording);
await using var provider = collection.BuildServiceProvider();
var factory = provider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient("name");
await client.GetAsync("https://httpcan.org/html");
recording.Resume();
await client.GetAsync("https://httpcan.org/json");
await Verify(recording.Sends)
.ScrubInlineDateTimes("R");Http Recording allows, when a method is being tested, for any http requests made as part of that method call to be recorded and verified.
Call HttpRecording.StartRecording(); before the method being tested is called.
The perform the verification as usual:
[Test]
public async Task TestHttpRecording()
{
Recording.Start();
var sizeOfResponse = await MethodThatDoesHttpCalls();
await Verify(
new
{
sizeOfResponse
})
.IgnoreMembers("Expires", "Date")
.ScrubLinesContaining("\"version\"");
}
static async Task<int> MethodThatDoesHttpCalls()
{
using var client = new HttpClient();
var jsonResult = await client.GetStringAsync("https://httpcan.org/json");
var ymlResult = await client.GetStringAsync("https://httpcan.org/xml");
return jsonResult.Length + ymlResult.Length;
}The requests/response pairs will be appended to the verified file.
{
target: {
sizeOfResponse: 792
},
httpCall: [
{
Status: Created,
Request: {
Uri: https://httpcan.org/json,
Headers: {}
},
Response: {
Status: 200 OK,
Headers: {
Access-Control-Allow-Credentials: true,
Alt-Svc: h3=":443",
cf-cache-status: DYNAMIC,
Connection: keep-alive,
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800},
Server: cloudflare,
Vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers
},
ContentHeaders: {
Content-Length: 274,
Content-Type: application/json
},
ContentStringParsed: {
slideshow: {
author: Yours Truly,
date: date of publication,
slides: [
{
title: Wake up to WonderWidgets!,
type: all
},
{
items: [
Why <em>WonderWidgets</em> are great,
Who <em>buys</em> WonderWidgets
],
title: Overview,
type: all
}
],
title: Sample Slide Show
}
}
}
},
{
Status: Created,
Request: {
Uri: https://httpcan.org/xml,
Headers: {}
},
Response: {
Status: 200 OK,
Headers: {
Access-Control-Allow-Credentials: true,
Alt-Svc: h3=":443",
cf-cache-status: DYNAMIC,
Connection: keep-alive,
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800},
Server: cloudflare,
Vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers
},
ContentHeaders: {
Content-Length: 518,
Content-Type: application/xml
},
ContentStringParsed: {
?xml: {
@version: 1.0,
@encoding: us-ascii
}/* A SAMPLE set of slides */,
slideshow: {
@title: Sample Slide Show,
@date: Date of publication,
@author: Yours Truly,
#comment: [],
slide: [
{
@type: all,
title: Wake up to WonderWidgets!
},
{
@type: all,
title: Overview,
item: [
{
#text: [
Why ,
are great
],
em: WonderWidgets
},
null,
{
#text: [
Who ,
WonderWidgets
],
em: buys
}
]
}
]
}
}
}
}
]
}The above usage results in the http calls being automatically added snapshot file. Calls can also be explicitly read and recorded using HttpRecording.FinishRecording(). This enables:
- Filtering what http calls are included in the snapshot.
- Only verifying a subset of information for each http call.
- Performing additional asserts on http calls.
For example:
[Test]
public async Task TestHttpRecordingExplicit()
{
Recording.Start();
var responseSize = await MethodThatDoesHttpCalls();
var httpCalls = Recording.Stop()
.Select(_ => _.Data)
.OfType<HttpCall>()
.ToList();
// Ensure all calls finished in under 5 seconds
var threshold = TimeSpan.FromSeconds(5);
foreach (var call in httpCalls)
{
IsTrue(call.Duration < threshold);
}
await Verify(
new
{
responseSize,
// Only use the Uri in the snapshot
httpCalls = httpCalls.Select(_ => _.Request.Uri)
});
}{
responseSize: 792,
httpCalls: [
https://httpcan.org/json,
https://httpcan.org/xml
]
}MockHttpClient allows mocking of http responses and recording of http requests.
The default behavior is to return a HttpResponseMessage with a status code of 200 OK.
[Test]
public async Task DefaultContent()
{
using var client = new MockHttpClient();
var result = await client.GetAsync("https://fake/get");
await Verify(result);
}{
Status: 200 OK
}Request-Response pairs can be verified using MockHttpClient.Calls
[Test]
public async Task ExplicitContent()
{
using var client = new MockHttpClient(
content: """{ "a": "b" }""",
mediaType: "application/json");
var result = await client.GetAsync("https://fake/get");
await Verify(result);
}[
{
Request: https://fake/get1,
Response: 200 Ok
},
{
Request: https://fake/get2,
Response: 200 Ok
}
]Always return an explicit StringContent and media-type:
[Test]
public async Task ExplicitContent()
{
using var client = new MockHttpClient(
content: """{ "a": "b" }""",
mediaType: "application/json");
var result = await client.GetAsync("https://fake/get");
await Verify(result);
}{
Status: 200 OK,
Content: {
Headers: {
Content-Length: 12,
Content-Type: application/json; charset=utf-8
},
Value: {
a: b
}
}
}Always return an explicit HttpStatusCode:
[Test]
public async Task ExplicitStatusCode()
{
using var client = new MockHttpClient(HttpStatusCode.Ambiguous);
var result = await client.GetAsync("https://fake/get");
await Verify(result);
}{
Status: 300 Multiple Choices
}Alwars return an explicit HttpResponseMessage:
[Test]
public async Task ExplicitResponse()
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("Hello")
};
using var client = new MockHttpClient(response);
var result = await client.GetAsync("https://fake/get");
await Verify(result);
}{
Status: 200 OK,
Content: {
Headers: {
Content-Length: 5,
Content-Type: text/plain; charset=utf-8
},
Value: Hello
}
}Use custom code to create a HttpResponseMessage base on a HttpRequestMessage:
[Test]
public async Task ResponseBuilder()
{
using var client = new MockHttpClient(request =>
{
var content = $"Hello to {request.RequestUri}";
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(content),
};
return response;
});
var result1 = await client.GetAsync("https://fake/get1");
var result2 = await client.GetAsync("https://fake/get2");
await Verify(new
{
result1,
result2
});
}{
result1: {
Status: 200 OK,
Content: {
Headers: {
Content-Length: 26,
Content-Type: text/plain; charset=utf-8
},
Value: Hello to https://fake/get1
}
},
result2: {
Status: 200 OK,
Content: {
Headers: {
Content-Length: 26,
Content-Type: text/plain; charset=utf-8
},
Value: Hello to https://fake/get2
}
}
}Use a sequence of HttpResponseMessage to return a sequence of requests:
[Test]
public async Task EnumerableResponses()
{
using var client = new MockHttpClient(
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("Hello")
},
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("World")
});
var result1 = await client.GetAsync("https://fake/get1");
var result2 = await client.GetAsync("https://fake/get2");
await Verify(new
{
result1,
result2
});
}{
result1: {
Status: 200 OK,
Content: {
Headers: {
Content-Length: 5,
Content-Type: text/plain; charset=utf-8
},
Value: Hello
}
},
result2: {
Status: 200 OK,
Content: {
Headers: {
Content-Length: 5,
Content-Type: text/plain; charset=utf-8
},
Value: World
}
}
}Interactions with MockHttpClient (in the form of Request and repsponse pairs) can optionally be included in Recording.
[Test]
public async Task RecordingMockInteractions()
{
using var client = new MockHttpClient(recording: true);
Recording.Start();
await client.GetStringAsync("https://fake/getOne");
await client.GetStringAsync("https://fake/getTwo");
await Verify();
}{
httpCall: [
{
Request: https://fake/getOne,
Response: 200 Ok
},
{
Request: https://fake/getTwo,
Response: 200 Ok
}
]
}Spider designed by marialuisa iborra from The Noun Project.
