Giter Site home page Giter Site logo

maxkagamine / moq.contrib.httpclient Goto Github PK

View Code? Open in Web Editor NEW
186.0 5.0 12.0 212 KB

A set of extension methods for mocking HttpClient and IHttpClientFactory with Moq.

License: MIT License

C# 100.00%
moq httpclient httpclientfactory dotnet dotnet-core unit-testing mock

moq.contrib.httpclient's Introduction

Moq.Contrib.HttpClient

NuGet ci build badge tested on badge

日本語

A set of extension methods for mocking HttpClient and IHttpClientFactory with Moq.

Mocking HttpClient has historically been surprisingly difficult, with the solution being to either create a wrapper to mock instead (at the cost of cluttering the code) or use a separate HTTP library entirely. This package provides extension methods for Moq that make handling HTTP requests as easy as mocking a service method.

Install

Install-Package Moq.Contrib.HttpClient

or dotnet add package Moq.Contrib.HttpClient

API

The library adds request/response variants of the standard Moq methods:

  • Setup → SetupRequest, SetupAnyRequest
  • SetupSequence → SetupRequestSequence, SetupAnyRequestSequence
  • Verify → VerifyRequest, VerifyAnyRequest
  • Returns(Async) → ReturnsResponse, ReturnsJsonResponse

Request

All Setup and Verify helpers have the same overloads, abbreviated here:

SetupAnyRequest()
SetupRequest([HttpMethod method, ]Predicate<HttpRequestMessage> match)
SetupRequest(string|Uri requestUrl[, Predicate<HttpRequestMessage> match])
SetupRequest(HttpMethod method, string|Uri requestUrl[, Predicate<HttpRequestMessage> match])

requestUrl matches the exact request URL, while the match predicate allows for more intricate matching, such as by query parameters or headers, and may be async as well to inspect the request body.

Response

The response helpers simplify sending a StringContent, JsonContent (using System.Text.Json), ByteArrayContent, StreamContent, or just a status code:

ReturnsResponse(HttpStatusCode statusCode[, HttpContent content], Action<HttpResponseMessage> configure = null)
ReturnsResponse([HttpStatusCode statusCode, ]string content, string mediaType = null, Encoding encoding = null, Action<HttpResponseMessage> configure = null))
ReturnsResponse([HttpStatusCode statusCode, ]byte[]|Stream content, string mediaType = null, Action<HttpResponseMessage> configure = null)
ReturnsJsonResponse<T>([HttpStatusCode statusCode, ]T value, JsonSerializerOptions options = null, Action<HttpResponseMessage> configure = null)

The statusCode defaults to 200 OK if omitted, and the configure action can be used to set response headers.

Examples

General usage

// All requests made with HttpClient go through its handler's SendAsync() which we mock
var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var client = handler.CreateClient();

// A simple example that returns 404 for any request
handler.SetupAnyRequest()
    .ReturnsResponse(HttpStatusCode.NotFound);

// Match GET requests to an endpoint that returns json (defaults to 200 OK)
handler.SetupRequest(HttpMethod.Get, "https://example.com/api/stuff")
    .ReturnsJsonResponse(model);

// Setting additional headers on the response using the optional configure action
handler.SetupRequest(HttpMethod.Get, "https://example.com/api/stuff")
    .ReturnsResponse(stream, configure: response =>
    {
        response.Content.Headers.LastModified = new DateTime(2022, 3, 9);
    });
💡 Why you should use MockBehavior.Strict for HttpClient

Consider the following:

handler.SetupRequest(HttpMethod.Get, "https://example.com/api/foos")
    .ReturnsJsonResponse(expected);

List<Foo> actual = await foosService.GetFoos();

actual.Should().BeEquivalentTo(expected);

This test fails unexpectedly with the following exception:

System.InvalidOperationException : Handler did not return a response message.

This is because Moq defaults to Loose mode which returns a default value if no setup matches, but HttpClient throws an InvalidOperationException if it receives null from the handler.

If we change it to MockBehavior.Strict:

- var handler = new Mock<HttpMessageHandler>();
+ var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);

We get a more useful exception that also includes the request that was made (here we see the URL was typo'd as "foo" instead of "foos"):

Moq.MockException : HttpMessageHandler.SendAsync(Method: GET, RequestUri: 'https://example.com/api/foo', Version: 1.1, Content: <null>, Headers:
{
}, System.Threading.CancellationToken) invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.

Matching requests by query params, headers, JSON body, etc.

// The request helpers can take a predicate for more intricate request matching
handler.SetupRequest(r => r.Headers.Authorization?.Parameter != authToken)
    .ReturnsResponse(HttpStatusCode.Unauthorized);

// The predicate can be async as well to inspect the request body
handler
    .SetupRequest(HttpMethod.Post, url, async request =>
    {
        // This setup will only match calls with the expected id
        var json = await request.Content.ReadFromJsonAsync<Model>();
        return json.Id == expected.Id;
    })
    .ReturnsResponse(HttpStatusCode.Created);

// This is particularly useful for matching URLs with query parameters
handler
    .SetupRequest(r =>
    {
        Url url = r.RequestUri;
        return url.Path == baseUrl.AppendPathSegment("endpoint") &&
            url.QueryParams["foo"].Equals("bar");
    })
    .ReturnsResponse("stuff");

The last example uses a URL builder library called Flurl to assist in checking the query string.

See "MatchesCustomPredicate" and "MatchesQueryParameters" in the request extension tests for further explanation, including various ways to inspect JSON requests.

Setting up a sequence of requests

Moq has two types of sequences:

  1. SetupSequence() which creates one setup that returns values in sequence, and
  2. InSequence().Setup() which creates multiple setups under When() conditions to ensure that they only match in order.

Both of these are supported; however, as with service methods, regular setups are generally most appropriate. The latter type can be useful, though, for cases where separate requests independent of each other (that is, not relying on information returned from the previous) must be made in a certain order.

See the sequence extensions tests for examples.

Composing responses based on the request body

The normal Returns method can be used together with the request helpers for more complex responses:

handler.SetupRequest("https://example.com/hello")
    .Returns(async (HttpRequestMessage request, CancellationToken _) => new HttpResponseMessage()
    {
        Content = new StringContent($"Hello, {await request.Content.ReadAsStringAsync()}")
    });

var response = await client.PostAsync("https://example.com/hello", new StringContent("world"));
var body = await response.Content.ReadAsStringAsync(); // Hello, world

Using IHttpClientFactory

Overview

It's common to see HttpClient wrapped in a using since it's IDisposable, but this is, rather counterintuitively, incorrect and can lead to the application eating up sockets. The standard advice is to reuse a single HttpClient, yet this has the drawback of not responding to DNS changes.

ASP.NET Core introduces an IHttpClientFactory which "manages the pooling and lifetime of underlying HttpClientMessageHandler instances to avoid common DNS problems that occur when manually managing HttpClient lifetimes." As a bonus, it also makes HttpClient's ability to plug in middleware more accessible — for example, using Polly to automatically handle retries and failures.

Mocking the factory

If your classes simply receive an HttpClient injected via IHttpClientFactory, the tests don't need to do anything different. If the constructor takes the factory itself instead, this can be mocked the same way:

var handler = new Mock<HttpMessageHandler>();
var factory = handler.CreateClientFactory();

This factory can then be passed into the class or injected via AutoMocker, and code calling factory.CreateClient() will receive clients backed by the mock handler.

Named clients

The CreateClientFactory() extension method returns a mock that's already set up to return a default client. If you're using named clients, a setup can be added like so:

// Configuring a named client (overriding the default)
Mock.Get(factory).Setup(x => x.CreateClient("api"))
    .Returns(() =>
    {
        var client = handler.CreateClient();
        client.BaseAddress = ApiBaseUrl;
        return client;
    });

Note: If you're getting a "Extension methods (here: HttpClientFactoryExtensions.CreateClient) may not be used in setup / verification expressions." error, make sure you're passing a string where it says "api" in the example.

Integration tests

For integration tests, rather than replace the IHttpClientFactory implementation in the service collection, it's possible to leverage the existing DI infrastructure and configure it to use a mock handler as the "primary" instead:

public class ExampleTests : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly WebApplicationFactory<Startup> factory;
    private readonly Mock<HttpMessageHandler> githubHandler = new();

    public ExampleTests(WebApplicationFactory<Startup> factory)
    {
        this.factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                // For the default (unnamed) client, use `Options.DefaultName`
                services.AddHttpClient("github")
                    .ConfigurePrimaryHttpMessageHandler(() => githubHandler.Object);
            });
        });
    }

This way, the integration tests use the same dependency injection and HttpClient configurations from ConfigureServices() (or Program.cs) as would be used in production.

See this sample ASP.NET Core app and its integration test for a working example.

More in-depth examples

The library's own unit tests have been written to serve as examples of the various helpers and different use cases:

License

MIT

moq.contrib.httpclient's People

Contributors

maxkagamine avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

moq.contrib.httpclient's Issues

Compiler warning CS8002: Referenced assembly does not have a strong name

I'm using Moq.Contrib.HttpClient in a .NET 8 unit test project where my assemblies need to be strong-named. I'm getting a warning referencing Moq.Contrib.HttpClient:

Warning CS8002 Referenced assembly 'Moq.Contrib.HttpClient, Version=1.4.0.0, Culture=neutral, PublicKeyToken=null' does not have a strong name.

All of the other assemblies I use for unit testing are strong-named and it'd be nice to see this package add a strong-name :) . I realize I can strong-name it myself but I think that is rather hacky.

Use in TestServer

Como lo uso con TestServer para simular

services.AddHttpClient(NamesEndPointConstants.DatosBasicos, config =>
            {
                config.BaseAddress = new Uri(configuration.GetUrlDatosBasicos());
            }).SetHandlerLifetime(TimeSpan.FromMinutes(5))
              .AddPolicyHandler(GetRetryPolicy());

por ejemplo.

Method for validating request Content

Is there a way to assert on the content of the request?

I want to validate that an external library using the mocked client is sending the correct info to the endpoint.

Thanks!

Unsupported expression: x => x.CreateClient() Extension methods (here: HttpClientFactoryExtensions.CreateClient) may not be used in setup / verification expressions.

The following code:

Mock<HttpMessageHandler> handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
IHttpClientFactory factory = handler.CreateClientFactory();

// Named clients can be configured as well (overriding the default)
Mock.Get(factory).Setup(x => x.CreateClient())
         .Returns(() =>
         {
                HttpClient client = handler.CreateClient();
                client.BaseAddress = new Uri("https://example.com");
                return client;
         });

Throwing the following error:

System.NotSupportedException : Unsupported expression: x => x.CreateClient()
Extension methods (here: HttpClientFactoryExtensions.CreateClient) may not be used in setup / verification expressions.

Note: I am trying to mock following code:

 private readonly HttpClient _skrillHttpClient;
 public SkrillService(IHttpClientFactory httpClientFactory)
 {
     _skrillHttpClient = httpClientFactory.CreateClient();
    _skrillHttpClient.BaseAddress = new Uri("https://www.skrill.com");
 }

Dotnet 7: The value cannot be null or empty. (Parameter 'mediaType')

In dotnet 7 if you do not set the mediaType, and allow the default null value to be used, then an exception will be thrown:

ArgumentException: "The value cannot be null or empty. (Parameter 'mediaType')"
at System.Net.Http.Headers.MediaTypeHeaderValue.CheckMediaTypeFormat(String mediaType, String parameterName)
at System.Net.Http.StringContent..ctor(String content, Encoding encoding, String mediaType)
...

I looked into this and StringContent has been changed. For the constructor that takes a mediaType string as the third parameter a null value will no longer be translated into the constant DefaultMediaType which is "text/plain". Consequently, when MediaTypeHeaderValue. CheckMediaTypeFormat() is called an ArgumentException will result.

dotnet/runtime@f132942

It looks to me like this could be fixed by updating RequestExtension.cs. The various ReturnsResponse methods that have a mediaType parameter with a default of null could change the default to "text/plain". Or the lines of code that directly create a StringContent could use the null-coalescing operator to return "text/plain" when mediaType is null.

Repeated calls to SetupRequest only returns reponse once, then fails

Hi,

Nice library! I am not a fan of extension methods but in this case didn't want to wrap the httpclient and factory stuff in an own interface, and this was a nice and quick solution.

We ran into one unexpected behaviour that seems inconsistent with Moq

Expected behavior:

  • repeated calls to SetupRequest keep returning the same response (you can verify how often the call is done with Times ...)

Actual behavior:

  • the first call after SetupRequest returns the response, but the second call doesn't return anything

Workaround:

  • we used SetupRequestSequence to return the same response twice.

(If I have time later I'd be willing to see if I could fix this myself with a unit test - looking at the tests helped me guess where the issue is, as I couldn't find a test setup for this scenario)

Issues with 'ReturnsJsonResponse' and Deserializing the Response-Content

If a Https calls Content.ReadAsStreamAsync() to get the Content and deserialize that Value, the returned object only contains null-values.

I created a Unit Test for this Issue:

  ```
  [Fact]
  public async Task RespondsWithJson_NoExtension()
  {
      // Once again using our music API from MatchesCustomPredicate, this time fetching songs
      var expected = new List<Song>()
      {
          new Song()
          {
              Title = "Lost One's Weeping",
              Artist = "Neru feat. Kagamine Rin",
              Album = "世界征服",
              Url = "https://youtu.be/mF4KTG4c-Ic"
          },
          new Song()
          {
              Title = "Gimme×Gimme",
              Artist = "八王子P, Giga feat. Hatsune Miku, Kagamine Rin",
              Album = "Hatsune Miku Magical Mirai 2020",
              Url = "https://youtu.be/IfEAtKW2qSI"
          }
      };

      handler.SetupRequest(HttpMethod.Get, "https://example.com/api/songs").ReturnsJsonResponse(expected);

      // Following line works as expected
      // handler.SetupRequest(HttpMethod.Get, "https://example.com/api/songs").ReturnsJsonResponse(expected, new JsonSerializerOptions());
      
      var actualMessage = await client.GetAsync("api/songs");
      var actualBody = await actualMessage.Content.ReadAsStreamAsync();
      var actual = await JsonSerializer.DeserializeAsync<List<Song>>(actualBody);
      
      actual.Should().BeEquivalentTo(expected);
  }

```

If I call .ReturnsJsonResponse(HttpStatusCode.OK, expectedResponse, new JsonSerializerOptions()); and provide any JsonSerializerOptions, the tests works s expected.

I'm not sure, if this is an actual error or a misleading documentation or ReturnsJsonResponse

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.