如何使用 Moq 在 .NET Core 2.1 中模拟新的 HttpClientFactory
How to mock the new HttpClientFactory in .NET Core 2.1 using Moq
.NET Core 2.1 附带了这个名为 HttpClientFactory
的新工厂,但我不知道如何模拟它以对包含 REST 服务调用的某些方法进行单元测试。
正在使用 .NET Core IoC 容器注入工厂,该方法所做的是从工厂创建一个新客户端:
var client = _httpClientFactory.CreateClient();
然后使用客户端从 REST 服务获取数据:
var result = await client.GetStringAsync(url);
HttpClientFactory
派生自 IHttpClientFactory
Interface 所以这只是创建接口的模拟的问题
var mockFactory = new Mock<IHttpClientFactory>();
根据您对客户端的需求,您需要将模拟设置为 return 和 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);
}
这段代码为我抛出了这个异常,
System.InvalidOperationException: 请求没有关联的配置对象或提供的配置为空。
因此将其包含在测试方法中,并且有效。
var configuration = new HttpConfiguration();
var request = new HttpRequestMessage();
request.SetConfiguration(configuration);
除了之前描述如何设置存根的 post 之外,您还可以使用 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>());
我使用的是来自@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'. This package may not be fully compatible with your project.
不使用 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;
}
来源:
另一种方法可能是创建一个额外的 class 来在内部调用该服务。这个 class 很容易被嘲笑。
这不是问题的 直接 答案,但它似乎不那么复杂且更易于测试。
对于那些希望通过 HttpClient
委托使用模拟 IHttpClientFactory
来避免在测试期间调用端点以及使用 .NET Core
版本的人获得相同结果的人高于 2.2
(似乎包含 HttpRequestMessageExtensions.CreateResponse
扩展名的 Microsoft.AspNet.WebApi.Core
包是 no longer available without relying upon the package targeting the .NET Core 2.2
) then the below adaption of 上面的答案在 .NET 5
.[=29 中对我有用=]
如果需要的话,可以直接使用 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);
}
}
至于testSetup
方法中的用法,同样,我直接使用了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);
}
.NET Core 2.1 附带了这个名为 HttpClientFactory
的新工厂,但我不知道如何模拟它以对包含 REST 服务调用的某些方法进行单元测试。
正在使用 .NET Core IoC 容器注入工厂,该方法所做的是从工厂创建一个新客户端:
var client = _httpClientFactory.CreateClient();
然后使用客户端从 REST 服务获取数据:
var result = await client.GetStringAsync(url);
HttpClientFactory
派生自 IHttpClientFactory
Interface 所以这只是创建接口的模拟的问题
var mockFactory = new Mock<IHttpClientFactory>();
根据您对客户端的需求,您需要将模拟设置为 return 和 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);
}
这段代码为我抛出了这个异常, System.InvalidOperationException: 请求没有关联的配置对象或提供的配置为空。
因此将其包含在测试方法中,并且有效。
var configuration = new HttpConfiguration();
var request = new HttpRequestMessage();
request.SetConfiguration(configuration);
除了之前描述如何设置存根的 post 之外,您还可以使用 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>());
我使用的是来自@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'. This package may not be fully compatible with your project.
不使用 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;
}
来源:
另一种方法可能是创建一个额外的 class 来在内部调用该服务。这个 class 很容易被嘲笑。 这不是问题的 直接 答案,但它似乎不那么复杂且更易于测试。
对于那些希望通过 如果需要的话,可以直接使用 至于test 最后,一个示例 HttpClient
委托使用模拟 IHttpClientFactory
来避免在测试期间调用端点以及使用 .NET Core
版本的人获得相同结果的人高于 2.2
(似乎包含 HttpRequestMessageExtensions.CreateResponse
扩展名的 Microsoft.AspNet.WebApi.Core
包是 no longer available without relying upon the package targeting the .NET Core 2.2
) then the below adaption of .NET 5
.[=29 中对我有用=]
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);
}