/Verify.Http

Extends Verify to allow verification of web bits.

Primary LanguageC#MIT LicenseMIT

Verify.Http

Build status NuGet Status

Extends Verify to allow verification of Http bits.

NuGet package

https://nuget.org/packages/Verify.Http/

Enable

Enable VerifyHttp once at assembly load time:

VerifyHttp.Enable();

snippet source | anchor

Converters

Includes converters for the following

  • HttpMethod
  • Uri
  • HttpHeaders
  • HttpContent
  • HttpRequestMessage
  • HttpResponseMessage

For example:

[Fact]
public async Task HttpResponse()
{
    using var client = new HttpClient();

    var result = await client.GetAsync("https://httpbin.org/get");

    await Verify(result);
}

snippet source | anchor

Results in:

{
  Version: 1.1,
  Status: 200 OK,
  Headers: {
    Access-Control-Allow-Credentials: true,
    Connection: keep-alive,
    Date: DateTime_1,
    Server: gunicorn/19.9.0
  },
  Content: {
    Headers: {
      Content-Type: application/json
    },
    Value: {
      args: {},
      headers: {
        Host: httpbin.org,
      },
      url: https://httpbin.org/get
    }
  },
  Request: https://httpbin.org/get
}

snippet source | anchor

HttpClient recording via Service

For code that does web calls via HttpClient, these calls can be recorded and verified.

Service that does http

Given a class that does some Http calls:

public class MyService
{
    HttpClient client;

    // Resolve a HttpClient. All http calls done at any
    // resolved client will be added to `recording.Sends`
    public MyService(HttpClient client) =>
        this.client = client;

    public Task MethodThatDoesHttp() =>
        // Some code that does some http calls
        client.GetAsync("https://httpbin.org/status/undefined");
}

snippet source | anchor

Add to IHttpClientBuilder

Http recording can be added to a IHttpClientBuilder:

var collection = new ServiceCollection();
collection.AddScoped<MyService>();
var httpBuilder = collection.AddHttpClient<MyService>();

// Adds a AddHttpClient and adds a RecordingHandler using AddHttpMessageHandler
var recording = httpBuilder.AddRecording();

await using var provider = collection.BuildServiceProvider();

var myService = provider.GetRequiredService<MyService>();

await myService.MethodThatDoesHttp();

await Verify(recording.Sends)
    // Ignore some headers that change per request
    .ModifySerialization(x => x.IgnoreMembers("Date"));

snippet source | anchor

Add globally

Http can also be added globally IHttpClientBuilder:

Note: This only seems to work in net5 and up.

var collection = new ServiceCollection();
collection.AddScoped<MyService>();

// Adds a AddHttpClient and adds a RecordingHandler using AddHttpMessageHandler
var (builder, recording) = collection.AddRecordingHttpClient();

await using var provider = collection.BuildServiceProvider();

var myService = provider.GetRequiredService<MyService>();

await myService.MethodThatDoesHttp();

await Verify(recording.Sends)
    // Ignore some headers that change per request
    .ModifySerialization(x => x.IgnoreMembers("Date"));

snippet source | anchor

Result

Will result in the following verified file:

[
  {
    RequestUri: https://httpbin.org/status/undefined,
    RequestMethod: GET,
    ResponseStatus: BadRequest,
    ResponseHeaders: {
      Access-Control-Allow-Credentials: true,
      Connection: keep-alive,
      Server: gunicorn/19.9.0
    },
    ResponseContent: Invalid status code
  }
]

snippet source | anchor

There a Pause/Resume semantics:

var collection = new ServiceCollection();
collection.AddScoped<MyService>();
var httpBuilder = collection.AddHttpClient<MyService>();

// Adds a AddHttpClient and adds a RecordingHandler using AddHttpMessageHandler
var recording = httpBuilder.AddRecording();

await using var provider = collection.BuildServiceProvider();

var myService = provider.GetRequiredService<MyService>();

// Recording is enabled by default. So Pause to stop recording
recording.Pause();
await myService.MethodThatDoesHttp();

// Resume recording
recording.Resume();
await myService.MethodThatDoesHttp();

await Verify(recording.Sends)
    .ModifySerialization(x => x.IgnoreMembers("Date"));

snippet source | anchor

If the AddRecordingHttpClient helper method does not meet requirements, the RecordingHandler can be explicitly added:

var collection = new ServiceCollection();

var builder = collection.AddHttpClient("name");

// Change to not recording at startup
var recording = new RecordingHandler(recording: false);

builder.AddHttpMessageHandler(() => recording);

await using var provider = collection.BuildServiceProvider();

var factory = provider.GetRequiredService<IHttpClientFactory>();

var client = factory.CreateClient("name");

await client.GetAsync("https://www.google.com/");

recording.Resume();
await client.GetAsync("https://httpbin.org/status/undefined");

await Verify(recording.Sends)
    .ModifySerialization(x => x.IgnoreMembers("Date"));

snippet source | anchor

Http Recording via listener

Http Recording allows, when a method is being tested, for any http requests made as part of that method call to be recorded and verified.

Supported in net5 and up

Usage

Call HttpRecording.StartRecording(); before the method being tested is called.

The perform the verification as usual:

[Fact]
public async Task TestHttpRecording()
{
    HttpRecording.StartRecording();

    var sizeOfResponse = await MethodThatDoesHttpCalls();

    await Verify(
            new
            {
                sizeOfResponse
            })
        .ModifySerialization(settings =>
        {
            //scrub some headers that are not consistent between test runs
            settings.IgnoreMembers("traceparent", "Date");
        });
}

static async Task<int> MethodThatDoesHttpCalls()
{
    using var client = new HttpClient();

    var jsonResult = await client.GetStringAsync("https://httpbin.org/json");
    var xmlResult = await client.GetStringAsync("https://httpbin.org/xml");
    return jsonResult.Length + xmlResult.Length;
}

snippet source | anchor

The requests/response pairs will be appended to the verified file.

{
  target: {
    sizeOfResponse: 951
  },
  httpCalls: [
    {
      Request: {
        Uri: https://httpbin.org/json,
        Headers: {}
      },
      Response: {
        Status: 200 OK,
        Headers: {
          Access-Control-Allow-Credentials: true,
          Connection: keep-alive,
          Server: gunicorn/19.9.0
        },
        ContentHeaders: {
          Content-Type: application/json
        },
        ContentStringParsed: {
          slideshow: {
            author: Yours Truly,
            date: date of publication,
            slides: [
              {
                title: Wake up to WonderWidgets!,
                type: all
              },
              {
                items: [
                  Why <em>WonderWidgets</em> are great,
                  Who <em>buys</em> WonderWidgets
                ],
                title: Overview,
                type: all
              }
            ],
            title: Sample Slide Show
          }
        }
      }
    },
    {
      Request: {
        Uri: https://httpbin.org/xml,
        Headers: {}
      },
      Response: {
        Status: 200 OK,
        Headers: {
          Access-Control-Allow-Credentials: true,
          Connection: keep-alive,
          Server: gunicorn/19.9.0
        },
        ContentHeaders: {
          Content-Type: application/xml
        },
        ContentStringParsed: {
          ?xml: {
            @version: 1.0,
            @encoding: us-ascii
          }/*  A SAMPLE set of slides  */,
          slideshow: {
            @title: Sample Slide Show,
            @date: Date of publication,
            @author: Yours Truly,
            #comment: [],
            slide: [
              {
                @type: all,
                title: Wake up to WonderWidgets!
              },
              {
                @type: all,
                title: Overview,
                item: [
                  {
                    #text: [
                      Why ,
                       are great
                    ],
                    em: WonderWidgets
                  },
                  null,
                  {
                    #text: [
                      Who ,
                       WonderWidgets
                    ],
                    em: buys
                  }
                ]
              }
            ]
          }
        }
      }
    }
  ]
}

snippet source | anchor

Explicit Usage

The above usage results in the http calls being automatically added snapshot file. Calls can also be explicitly read and recorded using HttpRecording.FinishRecording(). This enables:

  • Filtering what http calls are included in the snapshot.
  • Only verifying a subset of information for each http call.
  • Performing additional asserts on http calls.

For example:

[Fact]
public async Task TestHttpRecordingExplicit()
{
    HttpRecording.StartRecording();

    var sizeOfResponse = await MethodThatDoesHttpCalls();

    var httpCalls = HttpRecording.FinishRecording().ToList();

    // Ensure all calls finished in under 5 seconds
    var threshold = TimeSpan.FromSeconds(5);
    foreach (var call in httpCalls)
    {
        Assert.True(call.Duration < threshold);
    }

    await Verify(
        new
        {
            sizeOfResponse,
            // Only use the Uri in the snapshot
            httpCalls = httpCalls.Select(_ => _.Request.Uri)
        });
}

snippet source | anchor

Results in the following:

{
  sizeOfResponse: 951,
  httpCalls: [
    https://httpbin.org/json,
    https://httpbin.org/xml
  ]
}

snippet source | anchor

Icon

Spider designed by marialuisa iborra from The Noun Project.