Jds.TestingUtils.MockHttp
0.1.0
See the version list below for details.
dotnet add package Jds.TestingUtils.MockHttp --version 0.1.0
NuGet\Install-Package Jds.TestingUtils.MockHttp -Version 0.1.0
<PackageReference Include="Jds.TestingUtils.MockHttp" Version="0.1.0" />
paket add Jds.TestingUtils.MockHttp --version 0.1.0
#r "nuget: Jds.TestingUtils.MockHttp, 0.1.0"
// Install Jds.TestingUtils.MockHttp as a Cake Addin #addin nuget:?package=Jds.TestingUtils.MockHttp&version=0.1.0 // Install Jds.TestingUtils.MockHttp as a Cake Tool #tool nuget:?package=Jds.TestingUtils.MockHttp&version=0.1.0
TestingUtils: MockHttp
A mock HttpClient
builder.
Create mock HttpClient
instances to provide prearranged or derived test data.
Use
The Jds.TestingUtils.MockHttp.MockHttpBuilder
type is the core entry point for creating mock HttpClient
instances.
A detailed example of its use, extracted from project tests, is displayed as "Example" below. The XML documentation clarifies the expected results of the mock
HttpClient
arrangement.The
static HttpClient CreateCompleteApi()
method is the entrypoint showing how a mockHttpClient
is built using a fluent API.
The general pattern for creating mock HttpClient
instances is:
public static HttpClient CreateMockHttpClient()
{
return new MockHttpBuilder()
.WithHandler(messageCaseBuilder =>
messageCaseBuilder
.AcceptAll() // Use the most applicable .Accept*() method for the test case.
.RespondWith((responseBuilder, capturedRequest) => // The capturedRequest provides the received request's details, including its content, read as byte[]? and string?.
responseBuilder
.WithStatusCode(HttpStatusCode.OK) // Add response status code and headers using the fluent API.
.WithContent( // Use .WithContent() to set the HttpContent returned for accepted requests.
CreateHttpContent.TextPlain("Ok!") // The CreateHttpContent class provides HttpContent creation helpers.
)
)
) // Additional invocations of .WithHandler() can be chained to support multiple APIs or test cases.
.BuildHttpClient();
}
Example
using System.Collections.Concurrent;
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Text.Json.Serialization;
namespace Jds.TestingUtils.MockHttp.Tests.Unit;
internal static class MockApi
{
public static readonly Uri BaseUri = new("https://not-real", UriKind.Absolute);
public static readonly string PlainTextGetBody = "This is some plain text.";
public static Uri PlainTextGetRoute { get; } = new(BaseUri, "plaintext");
public static Uri SumIntsJsonPostRoute { get; } = new(BaseUri, "sum");
public static Uri StatefulGetRoute { get; } = new(BaseUri, "stateful");
public static Uri StatefulAddPostRoute { get; } = new(BaseUri, "stateful/add");
public static Uri StatefulRemovePostRoute { get; } = new(BaseUri, "stateful/remove");
/// <summary>
/// Create an <see cref="HttpClient" /> prearranged to simulate a mock API.
/// </summary>
/// <remarks>
/// <para>The following APIs are supported:</para>
/// <para>
/// <c>* https://not-real/</c> returns <see cref="HttpStatusCode.OK" /> to all requests and includes multiple custom
/// headers.
/// </para>
/// <para>
/// <c>HEAD https://not-real/plaintext</c> returns <see cref="HttpStatusCode.OK" />.
/// </para>
/// <para>
/// <c>GET https://not-real/plaintext</c> returns <see cref="HttpStatusCode.OK" /> and a plain text body,
/// <see cref="PlainTextGetBody" />.
/// </para>
/// <para>
/// <c>POST https://not-real/sum</c> expects a <see cref="SumIntsJsonRequest" /> JSON request body, and returns
/// <see cref="HttpStatusCode.OK" /> with a <see cref="SumIntsJsonResponse" /> JSON body.
/// </para>
/// <para>
/// <c>GET https://not-real/stateful</c> returns <see cref="HttpStatusCode.OK" /> with an int array JSON body.
/// </para>
/// <para>
/// <c>POST https://not-real/stateful/add</c> expects a <see cref="StatefulRequest" /> JSON request body, and returns
/// <see cref="HttpStatusCode.OK" />. Values are added to <c>GET https://not-real/stateful</c>.
/// </para>
/// <para>
/// <c>POST https://not-real/stateful/remove</c> expects a <see cref="StatefulRequest" /> JSON request body, and
/// returns <see cref="HttpStatusCode.OK" />. Values are removed from <c>GET https://not-real/stateful</c>.
/// </para>
/// </remarks>
/// <returns>A mocked <see cref="HttpClient" />.</returns>
public static HttpClient CreateCompleteApi()
{
ConcurrentBag<int> state = new();
return new MockHttpBuilder()
.WithHandler(RootRoute)
.WithHandler(PlaintextHead)
.WithHandler(PlaintextGet)
.WithHandler(SumPost)
.WithHandler(builder => StatefulGet(builder, state))
.WithHandler(builder => StatefulAddPostContainsValue(builder, state))
.WithHandler(StatefulAddPostMissingValue)
.WithHandler(builder => StatefulRemovePostContainsValue(builder, state))
.WithHandler(StatefulRemovePostMissingValue)
.BuildHttpClient();
}
/// <summary>
/// Arranges <paramref name="builder" /> to accept all requests for <see cref="BaseUri" />,
/// returning <see cref="HttpStatusCode.OK" /> and multiple custom headers.
/// </summary>
/// <param name="builder">A <see cref="MessageCaseHandlerBuilder" />.</param>
/// <returns>
/// <paramref name="builder" />
/// </returns>
private static MessageCaseHandlerBuilder RootRoute(MessageCaseHandlerBuilder builder)
{
return builder.AcceptUri(BaseUri)
.RespondWith((responseBuilder, message) =>
responseBuilder.WithStatusCode(HttpStatusCode.OK)
.WithHeader("custom-header", "custom-header singular value")
.WithHeader("multi-item-header", "multi-item-header value 1")
.WithHeader("multi-item-header", "multi-item-header value 2")
.WithHeader("multi-item-header", "multi-item-header value 3")
.WithTrailingHeader("custom-trailing-header", "custom-trailing-header singular value")
.WithTrailingHeader("multi-item-trailing-header", "multi-item-trailing-header value 1")
.WithTrailingHeader("multi-item-trailing-header", "multi-item-trailing-header value 2")
.WithTrailingHeader("multi-item-trailing-header", "multi-item-trailing-header value 3")
.WithVersion(new Version(2, 1, 3))
.WithReasonPhrase("OK")
.WithContent(new StringContent($"Response to uri: {message.RequestUri}", Encoding.UTF8,
MediaTypeNames.Text.Plain))
);
}
/// <summary>
/// Arranges <paramref name="builder" /> to accept a <see cref="HttpMethod.Head" /> <see cref="PlainTextGetRoute" />,
/// returning <see cref="HttpStatusCode.OK" />.
/// </summary>
/// <param name="builder">A <see cref="MessageCaseHandlerBuilder" />.</param>
/// <returns>
/// <paramref name="builder" />
/// </returns>
private static MessageCaseHandlerBuilder PlaintextHead(MessageCaseHandlerBuilder builder)
{
return builder.AcceptRoute(HttpMethod.Head, PlainTextGetRoute).RespondStatusCode(HttpStatusCode.OK);
}
/// <summary>
/// Arranges <paramref name="builder" /> to accept a <see cref="HttpMethod.Get" /> <see cref="PlainTextGetRoute" />,
/// returning <see cref="HttpStatusCode.OK" /> with a plain text <see cref="PlainTextGetBody" /> body.
/// </summary>
/// <param name="builder">A <see cref="MessageCaseHandlerBuilder" />.</param>
/// <returns>
/// <paramref name="builder" />
/// </returns>
private static MessageCaseHandlerBuilder PlaintextGet(MessageCaseHandlerBuilder builder)
{
return builder.AcceptRoute(HttpMethod.Get, PlainTextGetRoute)
.RespondStaticContent(
HttpStatusCode.OK,
new StringContent(PlainTextGetBody, Encoding.UTF8)
);
}
/// <summary>
/// Arranges <paramref name="builder" /> to sum the values sent in a <see cref="SumIntsJsonRequest" /> sent to
/// <see cref="HttpMethod.Post" /> <see cref="SumIntsJsonPostRoute" />, returning a <see cref="SumIntsJsonResponse" />.
/// </summary>
/// <param name="builder">A <see cref="MessageCaseHandlerBuilder" />.</param>
/// <returns>
/// <paramref name="builder" />
/// </returns>
private static MessageCaseHandlerBuilder SumPost(MessageCaseHandlerBuilder builder)
{
return builder.AcceptRoute(HttpMethod.Post, SumIntsJsonPostRoute)
.RespondDerivedContentJson(
(_, _) => Task.FromResult(HttpStatusCode.OK),
(sumIntsRequest, _) =>
Task.FromResult(new SumIntsJsonResponse { Sum = sumIntsRequest.Ints.Sum() }),
new SumIntsJsonRequest()
);
}
/// <summary>
/// Arranges <paramref name="builder" /> to return a JSON int array when receiving a GET <see cref="StatefulGetRoute" />.
/// </summary>
/// <param name="builder">A <see cref="MessageCaseHandlerBuilder" />.</param>
/// <param name="statefulStore">
/// A <see cref="ConcurrentBag{T}" /> which stores the persistent <see cref="int" />
/// collection throughout multiple requests.
/// </param>
/// <returns>
/// <paramref name="builder" />
/// </returns>
private static MessageCaseHandlerBuilder StatefulGet(MessageCaseHandlerBuilder builder,
ConcurrentBag<int> statefulStore)
{
return builder.AcceptRoute(HttpMethod.Get, StatefulGetRoute)
.RespondWith((responseBuilder, _) =>
responseBuilder
.WithStatusCode(HttpStatusCode.OK)
.WithContent(statefulStore.ToJsonHttpContent())
);
}
/// <summary>
/// Arranges <paramref name="builder" /> to add an <see cref="int" /> to <paramref name="statefulStore" /> when receiving
/// a POST <see cref="StatefulAddPostRoute" />.
/// </summary>
/// <param name="builder">A <see cref="MessageCaseHandlerBuilder" />.</param>
/// <param name="statefulStore">
/// A <see cref="ConcurrentBag{T}" /> which stores the persistent <see cref="int" />
/// collection throughout multiple requests.
/// </param>
/// <returns>
/// <paramref name="builder" />
/// </returns>
private static MessageCaseHandlerBuilder StatefulAddPostContainsValue(MessageCaseHandlerBuilder builder,
ConcurrentBag<int> statefulStore)
{
return builder
.AcceptRouteJson(
(method, uri, body) => body.Value != null && method == HttpMethod.Post && uri == StatefulAddPostRoute,
new StatefulRequest { Value = null }
)
.RespondDerivedContentJson(
valueDto => valueDto.Value.HasValue ? HttpStatusCode.OK : HttpStatusCode.BadRequest,
valueDto =>
{
statefulStore.Add(valueDto.Value ?? 0);
return "Added";
},
new StatefulRequest { Value = 0 }
);
}
/// <summary>
/// Arranges <paramref name="builder" /> to return <see cref="HttpStatusCode.BadRequest" /> when a
/// <c>POST</c> <see cref="StatefulAddPostRoute" /> request body contains a null <see cref="StatefulRequest.Value" />.
/// </summary>
/// <param name="builder">A <see cref="MessageCaseHandlerBuilder" />.</param>
/// <returns>
/// <paramref name="builder" />
/// </returns>
private static MessageCaseHandlerBuilder StatefulAddPostMissingValue(MessageCaseHandlerBuilder builder)
{
return builder.AcceptRoute(HttpMethod.Post, StatefulAddPostRoute)
.AcceptRouteJson(
(method, uri, body) => !body.Value.HasValue && method == HttpMethod.Post && uri == StatefulAddPostRoute,
new StatefulRequest { Value = null }
)
.RespondStaticContent(HttpStatusCode.BadRequest, CreateHttpContent.TextPlain(".value is required."));
}
/// <summary>
/// Arranges <paramref name="builder" /> to remove an <see cref="int" /> from <paramref name="statefulStore" /> when
/// receiving a POST <see cref="StatefulRemovePostRoute" />.
/// </summary>
/// <param name="builder">A <see cref="MessageCaseHandlerBuilder" />.</param>
/// <param name="statefulStore">
/// A <see cref="ConcurrentBag{T}" /> which stores the persistent <see cref="int" />
/// collection throughout multiple requests.
/// </param>
/// <returns>
/// <paramref name="builder" />
/// </returns>
private static MessageCaseHandlerBuilder StatefulRemovePostContainsValue(MessageCaseHandlerBuilder builder,
ConcurrentBag<int> statefulStore)
{
return builder
.AcceptRouteJson(
(method, uri, body) => body.Value != null && method == HttpMethod.Post && uri == StatefulRemovePostRoute,
new StatefulRequest { Value = null }
)
.RespondDerivedContentJson(
_ => HttpStatusCode.OK,
valueDto =>
{
var currentList = statefulStore.ToList();
statefulStore.Clear();
foreach (var value in currentList.Except(new[] { valueDto.Value ?? 0 }))
{
statefulStore.Add(value);
}
return "Removed";
},
new StatefulRequest { Value = 0 }
);
}
/// <summary>
/// Arranges <paramref name="builder" /> to return <see cref="HttpStatusCode.BadRequest" /> when a
/// <c>POST</c> <see cref="StatefulRemovePostRoute" /> request body contains a null <see cref="StatefulRequest.Value" />.
/// </summary>
/// <param name="builder">A <see cref="MessageCaseHandlerBuilder" />.</param>
/// <returns>
/// <paramref name="builder" />
/// </returns>
private static MessageCaseHandlerBuilder StatefulRemovePostMissingValue(MessageCaseHandlerBuilder builder)
{
return builder.AcceptRoute(HttpMethod.Post, StatefulRemovePostRoute)
.AcceptRouteJson(
(method, uri, body) => !body.Value.HasValue && method == HttpMethod.Post && uri == StatefulRemovePostRoute,
new StatefulRequest { Value = null }
)
.RespondStaticContent(HttpStatusCode.BadRequest, CreateHttpContent.TextPlain(".value is required."));
}
public record StatefulRequest
{
[JsonPropertyName("value")]
public int? Value { get; init; }
}
public record SumIntsJsonRequest
{
[JsonPropertyName("ints")]
public int[] Ints { get; init; } = Array.Empty<int>();
}
public record SumIntsJsonResponse
{
[JsonPropertyName("sum")]
public int Sum { get; init; }
}
}
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
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.