Jds.TestingUtils.MockHttp 0.3.0

dotnet add package Jds.TestingUtils.MockHttp --version 0.3.0                
NuGet\Install-Package Jds.TestingUtils.MockHttp -Version 0.3.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Jds.TestingUtils.MockHttp" Version="0.3.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Jds.TestingUtils.MockHttp --version 0.3.0                
#r "nuget: Jds.TestingUtils.MockHttp, 0.3.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Jds.TestingUtils.MockHttp as a Cake Addin
#addin nuget:?package=Jds.TestingUtils.MockHttp&version=0.3.0

// Install Jds.TestingUtils.MockHttp as a Cake Tool
#tool nuget:?package=Jds.TestingUtils.MockHttp&version=0.3.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 mock HttpClient 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last updated
0.3.0 2,477 8/7/2023
0.2.0 1,879 5/24/2022
0.1.0 582 3/20/2022