【问题标题】:Unable to Mock HttpClient PostAsync() in unit tests无法在单元测试中模拟 HttpClient PostAsync()
【发布时间】:2019-11-27 04:56:39
【问题描述】:

我正在使用 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) 不能用于设置/验证 表达式。

截图:

【问题讨论】:

标签: c# .net unit-testing moq xunit


【解决方案1】:

不要在代码中直接使用 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);
        }
    }
    

    【讨论】:

    • 相反,通过自定义HttpClientHanders 在HttpClient 中已经可以使用mocking。当使用 HttpClientFactory 时,它可以走得更远,模拟类型或命名的客户端
    • 我发现为中间抽象配置 DI 更简单。此外,它会导致更好的解耦,因为您不依赖于 Http 传输。如果需要,您可以稍后通过添加该类的额外实现将其完全替换为不同的连接。
    【解决方案3】:

    不可覆盖的成员(此处为: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. Unit Testing with the HttpClient
    2. How to mock HttpClient in your .NET / C# unit tests

    【讨论】:

    • 如果想为单元测试设置 HttpResponseMessage 很有用。谢谢!
    • 我仍然收到此错误消息:System.InvalidOperationException:提供了无效的请求 URI。请求 URI 必须是绝对 URI,或者必须设置 BaseAddress。
    • 对于未来的读者,请注意“SendAsync”不是印刷错误或“这是一个伪示例”。它将允许(在幕后)“PostAsync”工作......
    【解决方案4】:

    正如其他答案所解释的,您应该模拟 HttpMessageHandler 或 HttpClientFactory,而不是 HttpClient。这是一种常见的情况,有人为 both 案例创建了一个帮助程序库,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;
        });
    

    【讨论】:

      【解决方案5】:

      Visit Blog

      内置支持在 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")
             });
      

      【讨论】:

        猜你喜欢
        • 2020-11-19
        • 2020-09-19
        • 2018-06-16
        • 1970-01-01
        • 2019-12-13
        • 2014-04-16
        • 1970-01-01
        • 1970-01-01
        • 2018-08-07
        相关资源
        最近更新 更多