44

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

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

var client = _httpClientFactory.CreateClient();

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

var result = await client.GetStringAsync(url);
4

6 回答 6

69

HttpClientFactory是从IHttpClientFactoryInterface派生的所以只需要创建一个接口的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);
    }
}

取自我在这里给出的答案

使用 Moq参考Mock 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);
    }
}

并想测试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);
}
于 2019-01-17T01:15:41.933 回答
15

除了描述如何设置存根的上一篇文章之外,您还可以使用 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>());
于 2020-04-01T18:12:05.597 回答
5

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

警告 NU1701 包 'Microsoft.AspNet.WebApi.Core 5.2.7' 已使用 '.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' 而不是项目目标框架'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;
}

来源:

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

于 2021-02-18T08:23:40.527 回答
2

此代码为我引发了此异常 System.InvalidOperationException:请求没有关联的配置对象或提供的配置为空。

因此将其包含在测试方法中,并且可以正常工作。

var configuration = new HttpConfiguration();
var request = new HttpRequestMessage();
request.SetConfiguration(configuration);
于 2019-05-16T12:35:53.123 回答
2

对于那些希望通过IHttpClientFactory委托使用模拟来获得相同结果HttpClient以避免在测试期间调用端点并且使用.NET Core高于版本的人(其中包含扩展2.2的包似乎不再可用而不依赖包针对) 那么下面对Nkosi上面答案的改编对我有用。Microsoft.AspNet.WebApi.CoreHttpRequestMessageExtensions.CreateResponse.NET Core 2.2.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测试体通过了。

[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);
}
于 2021-09-07T20:36:03.237 回答
-1

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

于 2021-04-15T07:25:27.993 回答