Extensions for testing HTTP endpoints and deserializing the results. Currently works with XUnit.
Add the NuGet package:
dotnet add package Ardalis.HttpClientTestExtensionsIn your tests add this namespace:
usingArdalis.HttpClientTestExtensions;If you have existing test code that looks something like this:
publicclassDoctorsList:IClassFixture<CustomWebApplicationFactory<Startup>>{privatereadonlyHttpClient_client;privatereadonlyITestOutputHelper_outputHelper;publicDoctorsList(CustomWebApplicationFactory<Startup>factory,ITestOutputHelperoutputHelper){_client=factory.CreateClient();_outputHelper=outputHelper;}[Fact]publicasyncTaskReturns3Doctors(){varresponse=await_client.GetAsync("/api/doctors");response.EnsureSuccessStatusCode();varstringResponse=awaitresponse.Content.ReadAsStringAsync();_outputHelper.WriteLine(stringResponse);varresult=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]publicasyncTaskReturns3Doctors(){varresult=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]publicasyncTaskReturnsNotFoundGivenInvalidAuthorId(){intinvalidId=9999;varresponse=await_client.GetAsync(Routes.Authors.Get(invalidId));response.EnsureNotFound();}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 and return an object TAuthorDtoresult=awaitclient.GetAndDeserializeAsync("/authors/1",_testOutputHelper);// GET and return response as a stringstringresult=client.GetAndReturnStringAsync("/healthcheck");// GET and ensure response contains a substringstringresult=client.GetAndEnsureSubstringAsync("/healthcheck","OMG!");// GET and assert a 302 is returnedvarclient=_factory.CreateClient(newWebApplicationFactoryClientOptions(){AllowAutoRedirect=false});awaitclient.GetAndEnsureRedirectAsync("/oldone, "/newone");// GET and assert a 400 is returnedawaitclient.GetAndEnsureBadRequestAsync("/authors?page");// GET and assert a 401 is returnedawaitclient.GetAndEnsureUnauthorizedAsync("/authors/1");// GET and assert a 403 is returnedawaitclient.GetAndEnsureForbiddenAsync("/authors/1");// GET and assert a 404 is returnedawaitclient.GetAndEnsureNotFoundAsync("/authors/-1");// NOTE: There's a helper for this now, too (see below)varcontent=newStringContent(JsonSerializer.Serialize(dto),Encoding.UTF8,"application/json");// POST and return an object TAuthorDtoresult=awaitclient.PostAndDeserializeAsync("/authors",content);// POST and ensure response contains a substringstringresult=client.PostAndEnsureSubstringAsync("/authors",content,"OMG!");// POST and assert a 302 is returnedvarclient=_factory.CreateClient(newWebApplicationFactoryClientOptions(){AllowAutoRedirect=false});awaitclient.PostAndEnsureRedirectAsync("/oldone",content,"/newone");// POST and assert a 400 is returnedawaitclient.PostAndEnsureBadRequestAsync("/authors","banana");// POST and assert a 401 is returnedawaitclient.PostAndEnsureUnauthorizedAsync("/authors",content);// POST and assert a 403 is returnedawaitclient.PostAndEnsureForbiddenAsync("/authors",content);// POST and assert a 404 is returnedawaitclient.PostAndEnsureNotFoundAsync("/wrongendpoint",content)varcontent=newStringContent(JsonSerializer.Serialize(dto),Encoding.UTF8,"application/json");// PUT and return an object TAuthorDtoresult=awaitclient.PutAndDeserializeAsync("/authors/1",content);// PUT and ensure response contains a substringstringresult=client.PutAndEnsureSubstringAsync("/authors/1",content,"OMG!");// PUT and assert a 302 is returnedvarclient=_factory.CreateClient(newWebApplicationFactoryClientOptions(){AllowAutoRedirect=false});awaitclient.PutAndEnsureRedirectAsync("/oldone",content,"/newone");// PUT and assert a 400 is returnedawaitclient.PutAndEnsureBadRequestAsync("/authors/1","banana");// PUT and assert a 401 is returnedawaitclient.PutAndEnsureUnauthorizedAsync("/authors/1",content);// PUT and assert a 403 is returnedawaitclient.PutAndEnsureForbiddenAsync("/authors/1",content);// PUT and assert a 404 is returnedawaitclient.PutAndEnsureNotFoundAsync("/wrongendpoint",content)varcontent=newStringContent(JsonSerializer.Serialize(dto),Encoding.UTF8,"application/json");// PATCH and return an object TAuthorDtoresult=awaitclient.PatchAndDeserializeAsync("/authors/1",content);// PATCH and ensure response contains a substringstringresult=client.PatchAndEnsureSubstringAsync("/authors/1",content,"OMG!");// PATCH and assert a 302 is returnedvarclient=_factory.CreateClient(newWebApplicationFactoryClientOptions(){AllowAutoRedirect=false});awaitclient.PatchAndEnsureRedirectAsync("/oldone",content,"/newone");// PATCH and assert a 400 is returnedawaitclient.PatchAndEnsureBadRequestAsync("/authors/1","banana");// PATCH and assert a 401 is returnedawaitclient.PatchAndEnsureUnauthorizedAsync("/authors/1",content);// PATCH and assert a 403 is returnedawaitclient.PatchAndEnsureForbiddenAsync("/authors/1",content);// PATCH and assert a 404 is returnedawaitclient.PatchAndEnsureNotFoundAsync("/wrongendpoint",content)// DELETE and return an object TAuthorDtoresult=awaitclient.DeleteAndDeserializeAsync("/authors/1");// DELETE and ensure response contains a substringstringresult=client.DeleteAndEnsureSubstringAsync("/authors/1","OMG!");// DELETE and assert a 204 is returnedawaitclient.DeleteAndEnsureNoContentAsync("/authors/1");// DELETE and assert a 302 is returnedvarclient=_factory.CreateClient(newWebApplicationFactoryClientOptions(){AllowAutoRedirect=false});awaitclient.DeleteAndEnsureRedirectAsync("/oldone","/newone");// DELETE and assert a 400 is returnedawaitclient.DeleteAndEnsureBadRequestAsync("/authors/1");// DELETE and assert a 401 is returnedawaitclient.DeleteAndEnsureUnauthorizedAsync("/authors/1");// DELETE and assert a 403 is returnedawaitclient.DeleteAndEnsureForbiddenAsync("/authors/1");// DELETE and assert a 404 is returnedawaitclient.DeleteAndEnsureNotFoundAsync("/wrongendpoint");All of these methods are extensions on HttpResponseMessage.
// Assert a response has a status code of 204response.EnsureNoContent();// Assert a response has a status code of 302response.EnsureRedirect("/newone");// Assert a response has a status code of 400response.EnsureBadRequest();// Assert a response has a status code of 401response.EnsureUnauthorized();// Assert a response has a status code of 403response.EnsureForbidden();// Assert a response has a status code of 404response.EnsureNotFound();// Assert a response has a given status coderesponse.Ensure(HttpStatusCode.Created);// Assert a response contains a substingresponse.EnsureContainsAsync("OMG!",_testOutputHelper);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 typevarauthorDto=new("Steve");varcontent=StringContentHelpers.FromModelAsJson(authorDto);// now you can use this with a POST, PUT, etc.AuthorDtoresult=awaitclient.PostAndDeserializeAsync("/authors",content);// Or you can do it all in one line (assuming you already have the DTO)AuthorDtoresult=awaitclient.PostAndDeserializeAsync("/authors",StringContentHelpers.FromModelAsJson(authorDto));- 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