无法在单元测试中模拟HttpClient的PostAsync()方法

35

我正在使用xUnit和Moq编写测试用例。

我尝试模拟HttpClient的PostAsync(),但出现错误。

以下是用于模拟的代码:

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

错误:

不支持的表达式: repo => repo.PostAsync(It.IsAny(), It.IsAny()) 非可重写成员(这里指 HttpClient.PostAsync)不能在设置/验证表达式中使用。

截图:

图片描述


1
可能是 在单元测试中模拟 HttpClient 的重复问题。 - MarengoHue
请模拟 HttpClienthandler 或者 HttpClientFactory,而不是 HttpClient 本身。 - Panagiotis Kanavos
5个回答

48
非可重写成员(这里是HttpClient.PostAsync)不能在安装/验证表达式中使用。 我也尝试了与您相同的方式模拟HttpClient,并且收到了相同的错误消息。
解决方法: 不要模拟HttpClient,而是模拟HttpMessageHandler。 然后将mockHttpMessageHandler.Object赋给您的HttpClient,然后将其传递给您的产品代码类。这是因为HttpClient在幕后使用HttpMessageHandler:
// 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);

注意:您还需要

using Moq.Protected;

在你的测试文件顶部。

然后,您可以从测试中调用使用PostAsync的方法,PostAsync将返回HTTP状态OK响应:

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

优点: 模拟HttpMessageHandler意味着您的产品代码或测试代码中不需要额外的类。


有用的资源:

  1. 使用 HttpClient 进行单元测试
  2. 如何在 .NET / C# 单元测试中模拟 HttpClient

1
如果想要为单元测试设置HttpResponseMessage,这将非常有用。谢谢! - bakunet
1
我仍然收到这个错误信息: System.InvalidOperationException:提供了无效的请求URI。请求URI必须是绝对URI或必须设置BaseAddress。 - THE_GREAT_MARKER
7
请注意,针对未来的读者,“SendAsync”不是打印错误或“这是一个伪例子”。它将允许(在底层)“PostAsync”正常工作。 - granadaCoder

8

正如其他答案所解释的那样,您应该模拟HttpMessageHandler或HttpClientFactory,而不是HttpClient。这是一个非常常见的情况,有人为两种情况创建了一个帮助程序库,Moq.Contrib.HttpClient

从HttpClient的General Usage示例中复制:

// 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));

针对 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;
    });

4

访问博客

HttpRequestMessage 的 HttpMethod 和 RequestUri 属性支持内置的条件应用。通过使用 EndsWith 方法,我们可以实现对不同路径进行 HttpGet、HttpPost 和其他动词的模拟。

_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")
       });

3

不要在代码中直接使用 HttpClient 实例,使用 IHttpClientFactory 代替。

在测试中,您可以创建自己的 IHttpClientFactory 实现,它将返回连接到TestServer的 HttpClient。

下面是一个伪造工厂的示例:

public class InMemoryHttpClientFactory: IHttpClientFactory
{
    private readonly TestServer _server;

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

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

接下来,您可以在测试中设置一个TestServer,并让您的自定义IHttpClientFactory为该服务器创建客户端:

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

    [...]
}

2

你遇到的问题表明紧耦合,你可以通过引入中间抽象来解决它。你可能想要创建一个类来聚合HttpClient并通过接口暴露PostAsync()方法:

最初的回答

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

相反,通过自定义的 HttpClientHander,在 HttpClient 中已经可以进行模拟。当使用 HttpClientFactory 时,这甚至可以更进一步,模拟类型化或命名的客户端。 - Panagiotis Kanavos
我发现为中间抽象配置依赖注入更简单。此外,这会导致更好的解耦,因为您不依赖于传输是 Http。如果需要,您可以通过添加类的额外实现来完全替换它与不同的连接。 - MarengoHue

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接