.NET Core 2.1引入了一个名为HttpClientFactory
的新工厂,但我不知道如何模拟它以对包含REST服务调用的某些方法进行单元测试。
该工厂使用.NET Core IoC容器进行注入,方法所做的就是从工厂创建一个新客户端:
var client = _httpClientFactory.CreateClient();
然后使用客户端从REST服务获取数据:
var result = await client.GetStringAsync(url);
.NET Core 2.1引入了一个名为HttpClientFactory
的新工厂,但我不知道如何模拟它以对包含REST服务调用的某些方法进行单元测试。
该工厂使用.NET Core IoC容器进行注入,方法所做的就是从工厂创建一个新客户端:
var client = _httpClientFactory.CreateClient();
然后使用客户端从REST服务获取数据:
var result = await client.GetStringAsync(url);
HttpClientFactory
源自于 IHttpClientFactory
接口,因此只需创建接口的模拟即可。
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);
}
}
以下是我在这里给出的答案
假设您有一个控制器
[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);
}
除了之前的文章介绍如何设置桩对象,你也可以使用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>());
cf.CreateClient()
是一个扩展方法,Moq在模拟表达式中不允许使用它,所以我遇到了一个错误。要解决这个问题,可以使用clientFactoryMock.Setup(cf => cf.CreateClient(It.IsAny<string>()))
。 - Isaac Lyman.CreateClient(It.IsAny<string>())
。 - sourcx我使用了@Nkosi提供的示例,但在使用.NET 5
时,与HttpConfiguration
所需的包Microsoft.AspNet.WebApi.Core
出现了以下警告。
警告 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;
}
来源:
针对那些希望通过使用IHttpClientFactory
的模拟与HttpClient
委托实现避免在测试期间调用端点,并且正在使用高于2.2
的.NET Core
版本(在该版本中,似乎包含HttpRequestMessageExtensions.CreateResponse
扩展的Microsoft.AspNet.WebApi.Core
包已经不再可用,除非依赖于面向.NET Core 2.2
的包),则可以使用下面适应Nkosi上述答案的方法,在.NET 5
中为我工作。
如果只需要一个HttpRequestMessage
实例,则可以直接使用它。
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);
}
}
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);
}
这段代码在我的电脑上抛出了以下异常:
System.InvalidOperationException: 请求没有关联的配置对象或提供的配置为 null。
我在测试方法中添加了以下内容,问题得到了解决。
最初的回答:
var configuration = new HttpConfiguration();
var request = new HttpRequestMessage();
request.SetConfiguration(configuration);
就我个人而言,这是我使用 .NET 7 和 Azure Functions v4 实现的内容。它是一个可工作的、支持多请求的 HttpClientFactory 模拟。
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
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
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
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; }
}
该Azure函数已经设置了DI并使用了HttpClient对象。有关详细信息不在本篇文章的范围内。您可以通过谷歌搜索获取更多信息。
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;
}
}
简单的使用案例
public class UnitTest1
{
[Fact]
public void Test1()
{
var httpClientFactory = MockHttpClientFactory.Create("WhateverYouNamedIt", new MockHttpResponse());
var exception = Record.Exception(() => new Function1(httpClientFactory));
Assert.Null(exception);
}
}
更加真实的使用案例
[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);
}
request.CreateResponse()
的合适引用时遇到了问题 - 它可以在HttpRequestMessageExtensions中找到。 我通过向单元测试项目添加Microsoft.AspNet.WebApi.Core包来解决这个问题。 - Carter MusickCreateClient()
方法返回一个接口而不是具体的HttpClient类型。 - Tom Faltesek