GitHub - ardalis/HttpClientTestExtensions: Extensions for testing HTTP endpoints and deserializing the results. Currently works with XUnit. (original) (raw)
HttpClient Test Extensions
Extensions for testing HTTP endpoints and deserializing the results. Currently works with XUnit.
Installation
Add the NuGet package:
dotnet add package Ardalis.HttpClientTestExtensions
In your tests add this namespace:
using Ardalis.HttpClientTestExtensions;
Usage
If you have existing test code that looks something like this:
public class DoctorsList : IClassFixture<CustomWebApplicationFactory> { private readonly HttpClient _client; private readonly ITestOutputHelper _outputHelper;
public DoctorsList(CustomWebApplicationFactory factory, ITestOutputHelper outputHelper) { _client = factory.CreateClient(); _outputHelper = outputHelper; }
[Fact] public async Task Returns3Doctors() { var response = await _client.GetAsync("/api/doctors"); response.EnsureSuccessStatusCode(); var stringResponse = await response.Content.ReadAsStringAsync(); _outputHelper.WriteLine(stringResponse); var result = JsonSerializer.Deserialize(stringResponse, Constants.DefaultJsonOptions);
Assert.Equal(3, result.Doctors.Count());
Assert.Contains(result.Doctors, x => x.Name == "Dr. Smith");} }
You can now update the test to eliminate all but one of the lines prior to the assertions:
[Fact] public async Task Returns3Doctors() { var result = await _client.GetAndDeserialize("/api/doctors", _outputHelper);
Assert.Equal(3, result.Doctors.Count()); Assert.Contains(result.Doctors, x => x.Name == "Dr. Smith"); }
If you need to verify an endpoint returns a 404, you can use this approach:
[Fact] public async Task ReturnsNotFoundGivenInvalidAuthorId() { int invalidId = 9999;
var response = await _client.GetAsync(Routes.Authors.Get(invalidId));
response.EnsureNotFound(); }
List of Included Helper Methods
HttpClient
All of these methods are extensions on HttpClient; the following samples assume client is an HttpClient. All methods take an optional ITestOutputHelper, which is an xUnit type.
GET
// GET and return an object T AuthorDto result = await client.GetAndDeserializeAsync("/authors/1", _testOutputHelper);
// GET and return response as a string string result = client.GetAndReturnStringAsync("/healthcheck");
// GET and ensure response contains a substring string result = client.GetAndEnsureSubstringAsync("/healthcheck", "OMG!");
// GET and assert a 302 is returned var client = _factory.CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); await client.GetAndEnsureRedirectAsync("/oldone, "/newone");
// GET and assert a 400 is returned await client.GetAndEnsureBadRequestAsync("/authors?page");
// GET and assert a 401 is returned await client.GetAndEnsureUnauthorizedAsync("/authors/1");
// GET and assert a 403 is returned await client.GetAndEnsureForbiddenAsync("/authors/1");
// GET and assert a 404 is returned await client.GetAndEnsureNotFoundAsync("/authors/-1");
POST
// NOTE: There's a helper for this now, too (see below) var content = new StringContent(JsonSerializer.Serialize(dto), Encoding.UTF8, "application/json");
// POST and return an object T AuthorDto result = await client.PostAndDeserializeAsync("/authors", content);
// POST and ensure response contains a substring string result = client.PostAndEnsureSubstringAsync("/authors", content, "OMG!");
// POST and assert a 302 is returned var client = _factory.CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); await client.PostAndEnsureRedirectAsync("/oldone", content, "/newone");
// POST and assert a 400 is returned await client.PostAndEnsureBadRequestAsync("/authors", "banana");
// POST and assert a 401 is returned await client.PostAndEnsureUnauthorizedAsync("/authors", content);
// POST and assert a 403 is returned await client.PostAndEnsureForbiddenAsync("/authors", content);
// POST and assert a 404 is returned await client.PostAndEnsureNotFoundAsync("/wrongendpoint", content)
PUT
var content = new StringContent(JsonSerializer.Serialize(dto), Encoding.UTF8, "application/json");
// PUT and return an object T AuthorDto result = await client.PutAndDeserializeAsync("/authors/1", content);
// PUT and ensure response contains a substring string result = client.PutAndEnsureSubstringAsync("/authors/1", content, "OMG!");
// PUT and assert a 302 is returned var client = _factory.CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); await client.PutAndEnsureRedirectAsync("/oldone", content, "/newone");
// PUT and assert a 400 is returned await client.PutAndEnsureBadRequestAsync("/authors/1", "banana");
// PUT and assert a 401 is returned await client.PutAndEnsureUnauthorizedAsync("/authors/1", content);
// PUT and assert a 403 is returned await client.PutAndEnsureForbiddenAsync("/authors/1", content);
// PUT and assert a 404 is returned await client.PutAndEnsureNotFoundAsync("/wrongendpoint", content)
PATCH
var content = new StringContent(JsonSerializer.Serialize(dto), Encoding.UTF8, "application/json");
// PATCH and return an object T AuthorDto result = await client.PatchAndDeserializeAsync("/authors/1", content);
// PATCH and ensure response contains a substring string result = client.PatchAndEnsureSubstringAsync("/authors/1", content, "OMG!");
// PATCH and assert a 302 is returned var client = _factory.CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); await client.PatchAndEnsureRedirectAsync("/oldone", content, "/newone");
// PATCH and assert a 400 is returned await client.PatchAndEnsureBadRequestAsync("/authors/1", "banana");
// PATCH and assert a 401 is returned await client.PatchAndEnsureUnauthorizedAsync("/authors/1", content);
// PATCH and assert a 403 is returned await client.PatchAndEnsureForbiddenAsync("/authors/1", content);
// PATCH and assert a 404 is returned await client.PatchAndEnsureNotFoundAsync("/wrongendpoint", content)
DELETE
// DELETE and return an object T AuthorDto result = await client.DeleteAndDeserializeAsync("/authors/1");
// DELETE and ensure response contains a substring string result = client.DeleteAndEnsureSubstringAsync("/authors/1", "OMG!");
// DELETE and assert a 204 is returned await client.DeleteAndEnsureNoContentAsync("/authors/1");
// DELETE and assert a 302 is returned var client = _factory.CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); await client.DeleteAndEnsureRedirectAsync("/oldone", "/newone");
// DELETE and assert a 400 is returned await client.DeleteAndEnsureBadRequestAsync("/authors/1");
// DELETE and assert a 401 is returned await client.DeleteAndEnsureUnauthorizedAsync("/authors/1");
// DELETE and assert a 403 is returned await client.DeleteAndEnsureForbiddenAsync("/authors/1");
// DELETE and assert a 404 is returned await client.DeleteAndEnsureNotFoundAsync("/wrongendpoint");
HttpResponseMessage
All of these methods are extensions on HttpResponseMessage.
// Assert a response has a status code of 204 response.EnsureNoContent();
// Assert a response has a status code of 302 response.EnsureRedirect("/newone");
// Assert a response has a status code of 400 response.EnsureBadRequest();
// Assert a response has a status code of 401 response.EnsureUnauthorized();
// Assert a response has a status code of 403 response.EnsureForbidden();
// Assert a response has a status code of 404 response.EnsureNotFound();
// Assert a response has a given status code response.Ensure(HttpStatusCode.Created);
// Assert a response contains a substing response.EnsureContainsAsync("OMG!", _testOutputHelper);
StringContentHelpers
Extensions on HttpContent which you'll typically want to return a StringContent type as you serialize your DTO to JSON.
// Convert a C# DTO to a StringContent JSON type var authorDto = new ("Steve"); var content = StringContentHelpers.FromModelAsJson(authorDto);
// now you can use this with a POST, PUT, etc. AuthorDto result = await client.PostAndDeserializeAsync("/authors", content);
// Or you can do it all in one line (assuming you already have the DTO) AuthorDto result = await client.PostAndDeserializeAsync("/authors", StringContentHelpers.FromModelAsJson(authorDto));
Notes
- For now this is coupled with xUnit but if there is interest it could be split so the ITestOutputHelper dependency is removed/optional/swappable
- Additional helpers for other verbs are planned
- This is using System.Text.Json with default camelCase options that I've found most useful in my projects. This could be made extensible somehow as well.
- When making updates to this file make sure to also update the docs/README file that is embedded in the NuGet package