简体   繁体   English

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

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

.NET Core 2.1 comes with this new factory called HttpClientFactory , but I can't figure out how to mock it to unit test some methods that include REST service calls. .NET Core 2.1 附带了这个名为HttpClientFactory的新工厂,但我无法弄清楚如何模拟它以对包含 REST 服务调用的一些方法进行单元测试。

The factory is being injected using .NET Core IoC container, and what the method does is create a new client from the factory:使用 .NET 核心 IoC 容器注入工厂,该方法所做的是从工厂创建一个新的客户端:

var client = _httpClientFactory.CreateClient();

And then using the client to get data from a REST service:然后使用客户端从 REST 服务获取数据:

var result = await client.GetStringAsync(url);

The HttpClientFactory is derived from IHttpClientFactory Interface So it is just a matter of creating a mock of the interface HttpClientFactory派生自IHttpClientFactory接口所以这只是创建接口模拟的问题

var mockFactory = new Mock<IHttpClientFactory>();

Depending on what you need the client for, you would then need to setup the mock to return a HttpClient for the test.根据您需要客户端的用途,您需要设置模拟以返回HttpClient以进行测试。

This however requires an actual HttpClient .然而,这需要一个实际的HttpClient

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

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

IHttpClientFactory factory = mockFactory.Object;

The factory can then be injected into the dependent system under test when exercising the test.然后,在进行测试时,可以将工厂注入到被测依赖系统中。

If you do not want the client calling actual endpoints then you will need to create a fake delegate handler to intercept the requests.如果您不希望客户端调用实际端点,那么您将需要创建一个假委托处理程序来拦截请求。

Example of the handler stub used to fake the requests用于伪造请求的处理程序存根示例

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

Taken from an answer I gave here取自我在这里给出的答案

Reference Mock HttpClient using Moq使用 Moq参考Mock HttpClient

Suppose you have a controller假设你有一个控制器

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

and wanted to test the Get() action.并想测试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);
}

In addition to the previous post that describes how to setup a stub, you can just use Moq to setup the DelegatingHandler :除了上一篇描述如何设置存根的帖子之外,您还可以使用 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>());

I was using the example from @Nkosi but with .NET 5 I got the following warning with the package Microsoft.AspNet.WebApi.Core needed for HttpConfiguration .我正在使用来自@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'.警告 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'。 This package may not be fully compatible with your project.此包可能与您的项目不完全兼容。

Complete example without using HttpConfiguration :不使用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;
}

Source:来源:

HttpConfiguration from System.Web.Http in .NET 5 project .NET 5 项目中 System.Web.Http 的 HttpConfiguration

This code threw this exception for me, System.InvalidOperationException: The request does not have an associated configuration object or the provided configuration was null.此代码为我抛出了此异常 System.InvalidOperationException:请求没有关联的配置对象或提供的配置为空。

So included this in the test method, and it works.所以将它包含在测试方法中,并且它有效。

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

For those looking to achieve the same result of utilising a mock IHttpClientFactory with the HttpClient delegate to avoid making calls to endpoints during testing and who are using a version of .NET Core higher than 2.2 (where it seems the Microsoft.AspNet.WebApi.Core package containing the HttpRequestMessageExtensions.CreateResponse extension is no longer available without relying upon the package targeting the .NET Core 2.2 ) then the below adaption of Nkosi 's answer above has worked for me in .NET 5 .对于那些希望通过使用带有HttpClient委托的模拟IHttpClientFactory来避免在测试期间调用端点以及使用高于2.2.NET Core版本(似乎Microsoft.AspNet.WebApi.Core包含HttpRequestMessageExtensions.CreateResponse扩展的包在不依赖面向.NET Core 2.2的包的情况下不再可用)然后下面对上面Nkosi的回答的改编在.NET 5对我.NET 5

One can simply use an instance of HttpRequestMessage directly if that is all that is required.如果需要的话,可以直接使用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);
    }
}

As for the usage in the test Setup method, similarly, I've used an instance of HttpResponseMessage directly.至于在测试Setup方法中的使用,同样的,我直接使用了HttpResponseMessage一个实例。 In my case, the factoryMock is then passed into a custom Adapter which wraps around the HttpClient and is therefore set to use our fake HttpClient .在我的例子中, factoryMock然后被传递到一个自定义 Adapter ,它环绕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));

And finally, an example NUnit test body using this which passes.最后,一个使用 this 的示例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);
}

For what it's worth, this is my implementation using .NET 7 and Azure Functions v4.对于它的价值,这是我使用 .NET 7 和 Azure 函数 v4 的实现。 It is a workable, multi-request capable HttpClientFactory mock.它是一个可行的、支持多请求的 HttpClientFactory 模拟。

Unit Test Mock Setup单元测试模拟设置


MockHttpClientFactory MockHttpClientFactory

public class MockHttpClientFactory
{
    public static IHttpClientFactory Create(string name, MockHttpResponse response)
    {
        return Create(name, new List<MockHttpResponse> { response });
    }


    public static IHttpClientFactory Create(string name, List<MockHttpResponse> responses)
    {
                    
        Mock<HttpMessageHandler> messageHandler = SendAsyncHandler(responses);

        var mockHttpClientFactory = new Mock<IHttpClientFactory>();

        mockHttpClientFactory
            .Setup(x => x.CreateClient(name))
            .Returns(new HttpClient(messageHandler.Object)
            {
                BaseAddress = new Uri("https://mockdomain.mock")
            });

        return mockHttpClientFactory.Object;
    }


    private static Mock<HttpMessageHandler> SendAsyncHandler(List<MockHttpResponse> responses)
    {
        var messageHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);

        foreach(var response in responses)
        {
            messageHandler
                .Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync",
                    ItExpr.Is<HttpRequestMessage>(r => r.RequestUri!.PathAndQuery == response.UrlPart),
                    ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = response.StatusCode,
                    Content = (response.Response?.GetType() == typeof(string)
                        ? new StringContent(response.Response?.ToString() ?? "")
                        : new StringContent(JsonSerializer.Serialize(response.Response)))
                })
                .Verifiable();
        }               

        return messageHandler;
    }
}

MockHttpResponse MockHttpResponse

public class MockHttpResponse
{
    public MockHttpResponse()
    {           
    }

    public MockHttpResponse(string urlPart, object response, HttpStatusCode statusCode)
    {
        this.UrlPart = urlPart;
        this.Response = response;
        this.StatusCode = statusCode;
    }


    public string UrlPart { get; set; } = String.Empty;

    public object Response { get; set; } = default!;

    public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
}

MockHttpRequestData MockHttpRequestData

public class MockHttpRequestData
{ 
    public static HttpRequestData Create()
    {
        return Create<string>("");
    }   
    

    public static HttpRequestData Create<T>(T requestData) where T : class
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddFunctionsWorkerDefaults();

        var serializedData = JsonSerializer.Serialize(requestData);
        var bodyDataStream = new MemoryStream(Encoding.UTF8.GetBytes(serializedData));

        var context = new Mock<FunctionContext>();
        context.SetupProperty(context => context.InstanceServices, serviceCollection.BuildServiceProvider());

        var request = new Mock<HttpRequestData>(context.Object);
        request.Setup(r => r.Body).Returns(bodyDataStream);
        request.Setup(r => r.CreateResponse()).Returns(new MockHttpResponseData(context.Object));

        return request.Object;
    }
}

MockHttpResponseData MockHttpResponseData

public class MockHttpResponseData : HttpResponseData
{
    public MockHttpResponseData(FunctionContext functionContext) : base(functionContext)
    {           
    }
    

    public override HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;

    public override HttpHeadersCollection Headers { get; set; } = new HttpHeadersCollection();

    public override Stream Body { get; set; } = new MemoryStream();

    public override HttpCookies Cookies { get; }
}

Usage用法


The Azure Function Method Azure Function 方法

This azure function has been setup with DI and uses an HttpClient object. Details are out of scope for this post.此 azure function 已使用 DI 设置并使用 HttpClient object。此帖子的详细信息来自 scope。 You can Google for more info.您可以谷歌获取更多信息。

public class Function1
{
    private readonly HttpClient httpClient;


    public Function1(IHttpClientFactory httpClientFactory)
    {
        this.httpClient = httpClientFactory.CreateClient("WhateverYouNamedIt");
    }



    [Function("Function1")]
    public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
    {
        var httpResponse = await this.httpClient.GetAsync("/some-path");
        var httpResponseContent = await httpResponse.Content.ReadAsStringAsync();

        // do something with the httpResponse or Content

        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteStringAsync(httpResponseContent);
        
        return response;
    }               
}

Simple Use Case简单用例

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        var httpClientFactory = MockHttpClientFactory.Create("WhateverYouNamedIt", new MockHttpResponse());

        var exception = Record.Exception(() => new Function1(httpClientFactory));

        Assert.Null(exception);
    }
}

More Realistic Use Case更现实的用例

    [Fact]
    public async Task Test2()
    {
        var httpResponses = new List<MockHttpResponse>
        {
            new MockHttpResponse
            {
                UrlPart = "/some-path",
                Response = new { Name = "data" }
            }
        };

        var httpClientFactory = MockHttpClientFactory.Create("WhateverYouNamedIt", httpResponses);
        var httpRequestData = MockHttpRequestData.Create();

        var function1 = new Function1(httpClientFactory);
        var function1Response = await function1.Run(httpRequestData);
        function1Response.Body.Position = 0;

        using var streamReader = new StreamReader(function1Response.Body);
        var function1ResponseBody = await streamReader.ReadToEndAsync();
                
        Assert.Equal("{\"Name\":\"data\"}", function1ResponseBody);
    }

A different approach may be to create an extra class that will internally call the service.一种不同的方法可能是创建一个额外的类来在内部调用服务。 This class can be mocked easily.这个类很容易被嘲笑。 It is not a direct answer to the question, but it seems a lot less complex and more testable.这不是问题的直接答案,但它似乎不那么复杂且更易于测试。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM