[英]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 模拟。
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; }
}
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.