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

Mau*_*che 10 c# unit-testing moq asp.net-core-2.1 httpclientfactory

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

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

var client = _httpClientFactory.CreateClient();
Run Code Online (Sandbox Code Playgroud)

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

var result = await client.GetStringAsync(url);
Run Code Online (Sandbox Code Playgroud)

Nko*_*osi 20

HttpClientFactory源自IHttpClientFactory接口所以这只是一个创建接口的模拟的事

var mockFactory = new Mock<IHttpClientFactory>();
Run Code Online (Sandbox Code Playgroud)

然后,根据客户端需要的内容,您需要设置模拟以返回HttpClient测试的。

但是,这需要实际操作HttpClient

var clientHandlerStub = new DelegatingHandlerStub();
var client = new HttpClient(clientHandlerStub);

mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);

IHttpClientFactory factory = mockFactory.Object;
Run Code Online (Sandbox Code Playgroud)

然后,在进行测试时,可以将工厂注入到受测试的从属系统中。

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

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

取自我在这里给出的答案

使用Moq引用模拟HttpClient

假设您有一个控制器

[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);
    }
}
Run Code Online (Sandbox Code Playgroud)

并想测试该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);
}
Run Code Online (Sandbox Code Playgroud)

  • @MauricioAtanache 请注意,我设置了实际的接口成员,而不是扩展方法。扩展最终会调用接口方法。 (4认同)
  • 万一有人好奇,我会在为`request.CreateResponse()`找到适当的引用时遇到问题-可以在HttpRequestMessageExtensions中找到。我通过将Microsoft.AspNet.WebApi.Core包添加到项目中,将其添加到我的单元测试项目中 (3认同)
  • 当我尝试用 Moq 做这样的事情时,我收到 Moq 不支持扩展方法设置的错误,这就是我首先询问的原因,无论如何,我明天会尝试一下你的版本。 (2认同)
  • 哇。真痛苦……我希望`CreateClient()`方法返回一个接口,而不是具体的HttpClient类型。 (2认同)
  • 您应该将 `.Returns(client)` 更改为 `.Returns(() =&gt; new HttpClient(clientHandlerStub))` 以防止在第二次调用时返回已处理的 `HttpClient`。 (2认同)

Ogg*_*las 14

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

警告 NU1701 包“Microsoft.AspNet.WebApi.Core 5.2.7”已使用“.NETFramework,版本=v4.6.1,.NETFramework,版本=v4.6.2,.NETFramework,版本=v4.7,.NETFramework,版本”恢复=v4.7.1, .NETFramework,Version=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;
}
Run Code Online (Sandbox Code Playgroud)

来源:

.NET 5 项目中 System.Web.Http 的 HttpConfiguration


rdv*_*ren 13

除了上一篇描述如何设置存根的帖子之外,您还可以使用 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>());
Run Code Online (Sandbox Code Playgroud)

  • 我收到错误,因为 `cf.CreateClient()` 是一种扩展方法,Moq 不允许在模拟表达式中使用该方法。要解决此问题,请使用 clientFactoryMock.Setup(cf =&gt; cf.CreateClient(It.IsAny&lt;string&gt;()))`。 (6认同)

小智 8

IHttpClientFactory对于那些希望通过HttpClient委托使用模拟来避免在测试期间调用端点以及使用.NET Core高于版本的人2.2(其中似乎包含扩展的Microsoft.AspNet.WebApi.Core在不依赖包的情况下不再可用)获得相同结果的人针对) 那么Nkosi上面的答案的以下改编对我有用。HttpRequestMessageExtensions.CreateResponse.NET Core 2.2.NET 5

HttpRequestMessage如果需要的话,可以简单地使用 direct 的一个实例。

public class HttpHandlerStubDelegate : 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

至于测试Setup方法中的使用,同样,我直接使用了一个实例HttpResponseMessage。就我而言,factoryMock然后将 传入一个自定义适配器,该适配器环绕HttpClient,因此设置为使用我们的 fake 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));
Run Code Online (Sandbox Code Playgroud)

最后,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);
}
Run Code Online (Sandbox Code Playgroud)