Coder Social home page Coder Social logo

testingutils-mockhttp's Introduction

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; }
  }
}

testingutils-mockhttp's People

Contributors

jeremiahsanders avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.