Unable to Mock HttpClient PostAsync() in unit tests
Asked Answered
D

5

38

I am writing test cases using xUnit and Moq.

I am trying to mock PostAsync() of HttpClient, but I get an error.

Below is the code used for mocking:

   public TestADLS_Operations()
    {
        var mockClient = new Mock<HttpClient>();
        mockClient.Setup(repo => repo.PostAsync(It.IsAny<string>(), It.IsAny<HttpContent>())).Returns(() => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));

        this._iADLS_Operations = new ADLS_Operations(mockClient.Object);
    }

Error:

Unsupported expression: repo => repo.PostAsync(It.IsAny(), It.IsAny()) Non-overridable members (here: HttpClient.PostAsync) may not be used in setup / verification expressions.

Screenshot:

enter image description here

Dare answered 18/7, 2019 at 9:33 Comment(2)
Possible duplicate of Mocking HttpClient in unit testsAuction
Mockt the HttpClienthandler or the HttpClientFactory, not HttpClient itselfTelson
T
52

Non-overridable members (here: HttpClient.PostAsync) may not be used in setup / verification expressions.

I also tried to mock the HttpClient the same way you did, and I got the same error message.


Solution:

Instead of mocking the HttpClient, mock the HttpMessageHandler.

Then give the mockHttpMessageHandler.Object to your HttpClient, which you then pass to your product code class. This works because HttpClient uses HttpMessageHandler under the hood:

// Arrange
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK });

var client = new HttpClient(mockHttpMessageHandler.Object);
this._iADLS_Operations = new ADLS_Operations(client);

Note: You will also need a

using Moq.Protected;

at the top of your test file.

Then you can call your method that uses PostAsync from your test, and PostAsync will return an HTTP status OK response:

// Act
var returnedItem = this._iADLS_Operations.MethodThatUsesPostAsync(/*parameter(s) here*/);

Advantage: Mocking HttpMessageHandler means that you don't need extra classes in your product code or your test code.


Helpful resources:

  1. Unit Testing with the HttpClient
  2. How to mock HttpClient in your .NET / C# unit tests
Tarantula answered 13/1, 2020 at 21:46 Comment(3)
Useful if want to setup HttpResponseMessage for unit tests. Thanks!Carmine
I still got this error message: System.InvalidOperationException : An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.Collaborate
For future readers, note that "SendAsync" is NOT a misprint or "here's a pseudo example". It will allow (under the covers) "PostAsync" to work.................Vanhomrigh
T
8

As other answers explain, you should mock the HttpMessageHandler or the HttpClientFactory, not HttpClient. This is such a common scenario that someone created a helper library for both cases, Moq.Contrib.HttpClient.

Copying from the General Usage example for HttpClient :

// All requests made with HttpClient go through its handler's SendAsync() which we mock
var handler = new Mock<HttpMessageHandler>();
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")
    .ReturnsResponse(JsonConvert.SerializeObject(model), "application/json");

// Setting additional headers on the response using the optional configure action
handler.SetupRequest("https://example.com/api/stuff")
    .ReturnsResponse(bytes, configure: response =>
    {
        response.Content.Headers.LastModified = new DateTime(2018, 3, 9);
    })
    .Verifiable(); // Naturally we can use Moq methods as well

// Verify methods are provided matching the setup helpers
handler.VerifyAnyRequest(Times.Exactly(3));

For HttpClientFactory :

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

// Named clients can be configured as well (overriding the default)
Mock.Get(factory).Setup(x => x.CreateClient("api"))
    .Returns(() =>
    {
        var client = handler.CreateClient();
        client.BaseAddress = ApiBaseUrl;
        return client;
    });
Telson answered 14/1, 2020 at 10:45 Comment(0)
E
4

Visit Blog

There's inbuilt support to apply conditions on HttpMethod and RequestUri properties of HttpRequestMessage. This way we can mock HttpGet, HttpPost and other verbs for various paths using the EndsWith method as described below.

_httpMessageHandler.Protected()
      .Setup<Task<HttpResponseMessage>>("SendAsync", true,          
      *// Specify conditions for httpMethod and path
      ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get
           && req.RequestUri.AbsolutePath.EndsWith($"{path}"))),*
      ItExpr.IsAny<CancellationToken>())
      .ReturnsAsync(new HttpResponseMessage
      {
           StatusCode = HttpStatusCode.OK,
           Content = new StringContent("_0Kvpzc")
       });
Edwinedwina answered 18/8, 2020 at 15:35 Comment(0)
M
2

Instead of directly using an HttpClient instance in your code, use an IHttpClientFactory. In your tests, you can then create your own implementation of IHttpClientFactory that sends back a HttpClient which connects to a TestServer.

Here's an example of what your Fake Factory could look like:

public class InMemoryHttpClientFactory: IHttpClientFactory
{
    private readonly TestServer _server;

    public InMemoryHttpClientFactory(TestServer server)
    {
        _server = server;
    }

    public HttpClient CreateClient(string name)
    {
        return _server.CreateClient();
    }
}

You can then setup a TestServer in your tests and have your custom IHttpClientFactory create clients for that server:

public TestADLS_Operations()
{
    //setup TestServer
    IWebHostBuilder hostBuilder = new WebHostBuilder()
        .Configure(app => app.Run(
        async context =>
    {
        // set your response headers via the context.Response.Headers property
        // set your response content like this:
        byte[] content = Encoding.Unicode.GetBytes("myResponseContent");
        await context.Response.Body.WriteAsync(content);
    }));
    var testServer = new TestServer(hostBuilder)

    var factory = new InMemoryHttpClientFactory(testServer);
    _iADLS_Operations = new ADLS_Operations(factory);

    [...]
}
Mazur answered 18/7, 2019 at 9:50 Comment(1)
This is not something you want to do when writing unit tests, which I believe is why the author is asking to mock it.Benoit
A
2

The problem you are having indicates tight coupling, and you can resolve it by introducing an intermediate abstraction. You might want to create a class which aggregates the HttpClient and exposes the PostAsync() method via an interface:

// Now you mock this interface instead, which is a pretty simple task.
// I suggest also abstracting away from an HttpResponseMessage
// This would allow you to swap for any other transport in the future. All 
// of the response error handling could be done inside the message transport 
// class.  
public interface IMessageTransport
{
    Task SendMessageAsync(string message);
}

// In ADLS_Operations ctor:
public ADLS_Operations(IMessageTransport messageTransport)
{ 
    //...
}

public class HttpMessageTransport : IMessageTransport
{
    public HttpMessageTransport()
    {
        this.httpClient = //get the http client somewhere.
    }

    public Task SendMessageAsync(string message)
    {
        return this.httpClient.PostAsync(message);
    }
}
Auction answered 18/7, 2019 at 9:59 Comment(2)
On the contrary, mocking is already available in HttpClient through custom HttpClientHanders. When using HttpClientFactory this can go even farther, mocking typed or named clientsTelson
I find it simpler to configure the DI for an intermediate abstraction. Besides, it leads to better decoupling as you are not reliant on the transport being Http. You can replace it entirely with a different connection later if you need by adding extra implementations of the class.Auction

© 2022 - 2024 — McMap. All rights reserved.