【问题标题】:Mock HttpClient using Moq使用 Moq 模拟 HttpClient
【发布时间】:2017-06-23 07:32:13
【问题描述】:

我想对使用HttpClient 的类进行单元测试。我们在类构造函数中注入了HttpClient 对象。

public class ClassA : IClassA
{
    private readonly HttpClient _httpClient;

    public ClassA(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<HttpResponseMessage> SendRequest(SomeObject someObject)
    {
        //Do some stuff

        var request = new HttpRequestMessage(HttpMethod.Post, "http://some-domain.in");

        //Build the request

        var response = await _httpClient.SendAsync(request);

        return response;
    }
}

现在我们要对ClassA.SendRequest 方法进行单元测试。我们将Ms Test 用于单元测试框架,Moq 用于模拟。

当我们尝试模拟 HttpClient 时,它会抛出 NotSupportedException

[TestMethod]
public async Task SendRequestAsync_Test()
{
    var mockHttpClient = new Mock<HttpClient>();

    mockHttpClient.Setup(
        m => m.SendAsync(It.IsAny<HttpRequestMessage>()))
    .Returns(() => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
}

我们如何解决这个问题?

【问题讨论】:

    标签: c# unit-testing moq mstest dotnet-httpclient


    【解决方案1】:

    那个特定的重载方法不是虚拟的,所以不能被 Moq 覆盖。

    public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);
    

    这就是为什么它抛出NotSupportedException

    你要找的虚方法就是这个方法

    public virtual Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
    

    然而,模拟 HttpClient 并不像其内部消息处理程序看起来那么简单。

    我建议使用带有自定义消息处理程序存根的具体客户端,这将在伪造请求时提供更大的灵活性。

    这是一个委托处理程序存根的示例。

    public class DelegatingHandlerStub : DelegatingHandler {
        private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handlerFunc;
        public DelegatingHandlerStub() {
            _handlerFunc = (request, cancellationToken) => Task.FromResult(request.CreateResponse(HttpStatusCode.OK));
        }
    
        public DelegatingHandlerStub(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handlerFunc) {
            _handlerFunc = handlerFunc;
        }
    
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
            return _handlerFunc(request, cancellationToken);
        }
    }
    

    请注意,默认构造函数基本上是在做你之前试图模拟的事情。它还允许为请求提供更多自定义场景。

    使用存根,可以将测试重构为类似

    public async Task _SendRequestAsync_Test() {
        //Arrange           
        var handlerStub = new DelegatingHandlerStub();
        var client = new HttpClient(handlerStub);
        var sut = new ClassA(client);
        var obj = new SomeObject() {
            //Populate
        };
    
        //Act
        var response = await sut.SendRequest(obj);
    
        //Assert
        Assert.IsNotNull(response);
        Assert.IsTrue(response.IsSuccessStatusCode);
    }
    

    【讨论】:

      【解决方案2】:

      Moq 可以模拟出受保护的方法,例如 HttpMessageHandler 上的 SendAsync,您可以在其构造函数中提供给 HttpClient。

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

      复制自https://thecodebuzz.com/unit-test-mock-httpclientfactory-moq-net-core/

      【讨论】:

        【解决方案3】:

        使用 HttpClient 进行适当的模拟是一项艰巨的工作,因为它是在大多数人在 dotnet 中进行单元测试之前编写的。有时我会设置一个存根 HTTP 服务器,它根据与请求 url 匹配的模式返回预设响应,这意味着您测试真正的 HTTP 请求不是模拟,而是到 localhost 服务器。使用WireMock.net 使这变得非常简单,并且运行速度足够快,可以满足我的大部分单元测试需求。

        所以不要使用http://some-domain.in,而是在某个端口上使用本地主机服务器设置,然后:

        var server = FluentMockServer.Start(/*server and port can be setup here*/);
        server.Given(
              Request.Create()
              .WithPath("/").UsingPost()
           )
           .RespondWith(
               Response.Create()
               .WithStatusCode(200)
               .WithHeader("Content-Type", "application/json")
               .WithBody("{'attr':'value'}")
           );
        

        您可以找到更多详细信息和guidance on using wiremock in tests here.

        【讨论】:

          【解决方案4】:

          我最近不得不模拟 HttpClient,我使用了Moq.Contrib.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));
          

          更多信息,请查看作者的博文here

          【讨论】:

            猜你喜欢
            • 2019-09-27
            • 1970-01-01
            • 2011-06-13
            • 2019-09-11
            • 2010-11-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多