Ardalis.HttpClientTestExtensions
4.2.0
dotnet add package Ardalis.HttpClientTestExtensions --version 4.2.0
NuGet\Install-Package Ardalis.HttpClientTestExtensions -Version 4.2.0
<PackageReference Include="Ardalis.HttpClientTestExtensions" Version="4.2.0" />
paket add Ardalis.HttpClientTestExtensions --version 4.2.0
#r "nuget: Ardalis.HttpClientTestExtensions, 4.2.0"
// Install Ardalis.HttpClientTestExtensions as a Cake Addin #addin nuget:?package=Ardalis.HttpClientTestExtensions&version=4.2.0 // Install Ardalis.HttpClientTestExtensions as a Cake Tool #tool nuget:?package=Ardalis.HttpClientTestExtensions&version=4.2.0
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<Startup>>
{
private readonly HttpClient _client;
private readonly ITestOutputHelper _outputHelper;
public DoctorsList(CustomWebApplicationFactory<Startup> 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<ListDoctorResponse>(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<ListDoctorResponse>("/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");
// GET and assert a 405 is returned
await client.GetAndEnsureMethodNotAllowedAsync("/wrongendpoint", content)
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)
// POST and assert a 405 is returned
await client.PostAndEnsureMethodNotAllowedAsync("/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)
// PUT and assert a 405 is returned
await client.PutAndEnsureMethodNotAllowedAsync("/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");
// DELETE and assert a 405 is returned
await client.DeleteAndEnsureMethodNotAllowedAsync("/wrongendpoint", content)
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 status code of 405
response.EnsureMethodNotAllowed();
// 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.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- Microsoft.AspNetCore.Mvc.Testing (>= 6.0.8)
- System.Text.Json (>= 6.0.5)
- xunit.abstractions (>= 2.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories (4)
Showing the top 4 popular GitHub repositories that depend on Ardalis.HttpClientTestExtensions:
Repository | Stars |
---|---|
ardalis/CleanArchitecture
Clean Architecture Solution Template: A starting point for Clean Architecture with ASP.NET Core
|
|
ardalis/ApiEndpoints
A project for supporting API Endpoints in ASP.NET Core web applications.
|
|
ardalis/WebApiBestPractices
Resources related to my Pluralsight course on this topic.
|
|
DevBetterCom/DevBetterWeb
A simple web application for devBetter
|
Adding PATCH verb support by @janskola in #37