【问题标题】:How to mock the new HttpClientFactory in .NET Core 2.1 using Moq如何使用 Moq 在 .NET Core 2.1 中模拟新的 HttpClientFactory
【发布时间】:2019-06-11 03:43:12
【问题描述】:

.NET Core 2.1 附带了这个名为 HttpClientFactory 的新工厂,但我不知道如何模拟它来对包括 REST 服务调用的一些方法进行单元测试。

工厂正在使用 .NET Core IoC 容器注入,该方法的作用是从工厂创建一个新客户端:

var client = _httpClientFactory.CreateClient();

然后使用客户端从 REST 服务获取数据:

var result = await client.GetStringAsync(url);

【问题讨论】:

    标签: c# unit-testing moq asp.net-core-2.1 httpclientfactory


    【解决方案1】:

    HttpClientFactory 是从IHttpClientFactory Interface 派生的,所以只需要创建一个接口的mock

    var mockFactory = new Mock<IHttpClientFactory>();
    

    根据您对客户端的需求,您需要设置模拟以返回 HttpClient 进行测试。

    但这需要一个实际的HttpClient

    var clientHandlerStub = new DelegatingHandlerStub();
    var client = new HttpClient(clientHandlerStub);
    
    mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);
    
    IHttpClientFactory factory = mockFactory.Object;
    

    然后可以在执行测试时将工厂注入到被测依赖系统中。

    如果您不希望客户端调用实际的端点,那么您将需要创建一个假委托处理程序来拦截请求。

    用于伪造请求的处理程序存根示例

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

    摘自我在这里给出的答案

    参考Mock HttpClient using Moq

    假设你有一个控制器

    [Route("api/[controller]")]
    public class ValuesController : Controller {
        private readonly IHttpClientFactory _httpClientFactory;
    
        public ValuesController(IHttpClientFactory httpClientFactory) {
            _httpClientFactory = httpClientFactory;
        }
    
        [HttpGet]
        public async Task<IActionResult> Get() {
            var client = _httpClientFactory.CreateClient();
            var url = "http://example.com";
            var result = await client.GetStringAsync(url);
            return Ok(result);
        }
    }
    

    并想测试Get() 操作。

    public async Task Should_Return_Ok() {
        //Arrange
        var expected = "Hello World";
        var mockFactory = new Mock<IHttpClientFactory>();
        var configuration = new HttpConfiguration();
        var clientHandlerStub = new DelegatingHandlerStub((request, cancellationToken) => {
            request.SetConfiguration(configuration);
            var response = request.CreateResponse(HttpStatusCode.OK, expected);
            return Task.FromResult(response);
        });
        var client = new HttpClient(clientHandlerStub);
        
        mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);
        
        IHttpClientFactory factory = mockFactory.Object;
        
        var controller = new ValuesController(factory);
        
        //Act
        var result = await controller.Get();
        
        //Assert
        result.Should().NotBeNull();
        
        var okResult = result as OkObjectResult;
        
        var actual = (string) okResult.Value;
        
        actual.Should().Be(expected);
    }
    

    【讨论】:

    • 当我尝试用 Moq 做这样的事情时,我收到一个来自 Moq 的错误,不支持扩展方法的设置,这就是我一开始问的原因,无论如何,我会给出一个明天试试你的版本。
    • @MauricioAtanache 请注意,我设置了实际的接口成员,而不是扩展方法。扩展最终会调用接口方法。
    • 如果有人好奇,我在为request.CreateResponse() 找到合适的参考资料时遇到了问题——可以在 HttpRequestMessageExtensions 中找到。我通过将 Microsoft.AspNet.WebApi.Core 包添加到项目中,将此添加到我的单元测试项目中
    • 哇。多么痛苦……我希望 CreateClient() 方法返回一个接口而不是具体的 HttpClient 类型。
    • 您应该将 .Returns(client) 更改为 .Returns(() =&gt; new HttpClient(clientHandlerStub)) 以防止在第二次调用时返回已处理的 HttpClients。
    【解决方案2】:

    除了描述如何设置存根的上一篇文章之外,您可以使用 Moq 设置DelegatingHandler

    var clientHandlerMock = new Mock<DelegatingHandler>();
    clientHandlerMock.Protected()
        .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK))
        .Verifiable();
    clientHandlerMock.As<IDisposable>().Setup(s => s.Dispose());
    
    var httpClient = new HttpClient(clientHandlerMock.Object);
    
    var clientFactoryMock = new Mock<IHttpClientFactory>(MockBehavior.Strict);
    clientFactoryMock.Setup(cf => cf.CreateClient()).Returns(httpClient).Verifiable();
    
    clientFactoryMock.Verify(cf => cf.CreateClient());
    clientHandlerMock.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
    

    【讨论】:

      【解决方案3】:

      我使用的是来自 @Nkosi 的示例,但使用 .NET 5 我收到了以下警告,其中包含 HttpConfiguration 所需的包 Microsoft.AspNet.WebApi.Core

      警告 NU1701 包“Microsoft.AspNet.WebApi.Core 5.2.7”是 使用 '.NETFramework,Version=v4.6.1, .NETFramework,版本=v4.6.2,.NETFramework,版本=v4.7, .NETFramework,版本=v4.7.1, .NETFramework,版本=v4.7.2, .NETFramework,Version=v4.8' 代替项目目标框架 'net5.0'。此软件包可能与您的项目不完全兼容。

      不使用HttpConfiguration的完整示例:

      private LoginController GetLoginController()
      {
          var expected = "Hello world";
          var mockFactory = new Mock<IHttpClientFactory>();
      
          var mockMessageHandler = new Mock<HttpMessageHandler>();
          mockMessageHandler.Protected()
              .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
              .ReturnsAsync(new HttpResponseMessage
              {
                  StatusCode = HttpStatusCode.OK,
                  Content = new StringContent(expected)
              });
      
          var httpClient = new HttpClient(mockMessageHandler.Object);
      
          mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);
      
          var logger = Mock.Of<ILogger<LoginController>>();
      
          var controller = new LoginController(logger, mockFactory.Object);
      
          return controller;
      }
      

      来源:

      HttpConfiguration from System.Web.Http in .NET 5 project

      【讨论】:

        【解决方案4】:

        这段代码为我抛出了这个异常, System.InvalidOperationException:请求没有关联的配置对象或提供的配置为空。

        所以将它包含在测试方法中,它可以工作。

        var configuration = new HttpConfiguration();
        var request = new HttpRequestMessage();
        request.SetConfiguration(configuration);
        

        【讨论】:

          【解决方案5】:

          对于那些希望通过HttpClient 委托使用模拟IHttpClientFactory 来避免在测试期间调用端点以及使用高于2.2.NET Core 版本的人(其中它似乎包含HttpRequestMessageExtensions.CreateResponse 扩展名的Microsoft.AspNet.WebApi.Core 包是no longer available without relying upon the package targeting the .NET Core 2.2) 那么下面对Nkosi 的回答在.NET 5 中对我有用。

          如果需要的话,可以直接使用HttpRequestMessage 的实例。

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

          至于在测试Setup方法中的使用,同样,我直接使用了HttpResponseMessage的一个实例。在我的例子中,factoryMock 然后被传递到一个自定义适配器中,该适配器环绕在 HttpClient 周围,因此设置为使用我们的假 HttpClient

          var expected = @"{ ""foo"": ""bar"" }";
          var clientHandlerStub = new HttpHandlerStubDelegate((request, cancellationToken) => {
              var response = new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent(expected) };
              return Task.FromResult(response);
          });
          
          var factoryMock = new Mock<IHttpClientFactory>();
          factoryMock.Setup(m => m.CreateClient(It.IsAny<string>()))
              .Returns(() => new HttpClient(clientHandlerStub));
          

          最后,一个示例 NUnit 使用 this 的测试体通过了。

          [Test]
          public async Task Subject_Condition_Expectation()
          {
              var expected = @"{ ""foo"": ""bar"" }";
          
              var result = await _myHttpClientWrapper.GetAsync("https://www.example.com/api/stuff");
              var actual = await result.Content.ReadAsStringAsync();
          
              Assert.AreEqual(expected, actual);
          }
          

          【讨论】:

            【解决方案6】:

            另一种方法可能是创建一个额外的类,该类将在内部调用该服务。这个类可以很容易地被嘲笑。 这不是对问题的直接答案,但它似乎不那么复杂且更易于测试。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2017-12-26
              • 1970-01-01
              • 1970-01-01
              • 2011-02-08
              • 2019-01-14
              相关资源
              最近更新 更多