如何使用 Moq 在 .NET Core 2.1 中模拟新的 HttpClientFactory

How to mock the new HttpClientFactory in .NET Core 2.1 using Moq

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

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

var client = _httpClientFactory.CreateClient();

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

var result = await client.GetStringAsync(url);

HttpClientFactory 派生自 IHttpClientFactory Interface 所以这只是创建接口的模拟的问题

var mockFactory = new Mock<IHttpClientFactory>();

根据您对客户端的需求,您需要将模拟设置为 return 和 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);
    }
}

取自我在这里给出的答案

引用

假设你有一个控制器

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

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

因此将其包含在测试方法中,并且有效。

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

除了之前描述如何设置存根的 post 之外,您还可以使用 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>());

我使用的是来自@Nkosi 的示例,但是对于 .NET 5 我收到了以下警告,其中包含 HttpConfiguration.

所需的软件包 Microsoft.AspNet.WebApi.Core

Warning NU1701 Package 'Microsoft.AspNet.WebApi.Core 5.2.7' was restored using '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8' instead of the project target framework 'net5.0'. This package may not be fully compatible with your project.

不使用 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;
}

来源:

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

对于那些希望通过 HttpClient 委托使用模拟 IHttpClientFactory 来避免在测试期间调用端点以及使用 .NET Core 版本的人获得相同结果的人高于 2.2(似乎包含 HttpRequestMessageExtensions.CreateResponse 扩展名的 Microsoft.AspNet.WebApi.Core 包是 no longer available without relying upon the package targeting the .NET Core 2.2) then the below adaption of 上面的答案在 .NET 5.[=29 中对我有用=]

如果需要的话,可以直接使用 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);
    }
}

至于testSetup方法中的用法,同样,我直接使用了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 测试主体使用它通过。

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