有几种不同的方法可以模拟一个
HttpClient
。以下是我在使用
xUnit
进行一些 POC 后决定采用单一解决方案(
Moq.Contrib.HttpClient
)的一些方法。请注意,每个框架的能力远不止下面所示的内容; 我为了清晰起见,保持每个示例简洁。
Moq(独立使用)
如果您熟悉使用 Moq 框架,那么这相对比较简单。 "技巧" 是在 HttpClient
内部模拟 HttpMessageHandler
,而不是模拟 HttpClient
本身。 注意:最好在模拟中使用 MockBehavior.Strict
,以便您被警告任何您没有明确模拟并且期望的调用。
RichardSzalay.MockHttp
RichardSzalay.MockHttp是另一个流行的解决方案。我过去用过这个,但发现它比Moq.Contrib.HttpClient
略显繁琐。这里有两种不同的模式可以使用。 Richard在这里描述了何时使用其中一种。
Moq.Contrib.HttpClient
像单独使用 Moq
的解决方案一样,如果您熟悉使用 Moq 框架,那么这相对比较简单。我发现这个解决方案更直接、代码更少。这是我选择使用的解决方案。请注意,此解决方案需要与 Moq
本身分开安装 - Moq.Contrib.HttpClient
WireMock.Net
作为一个新来者,WireMock.net 正变得越来越受欢迎。如果您正在编写集成测试,并且实际进行端点调用而不是模拟,则这将是一个合理的替代 Microsoft.AspNetCore.TestHost
的解决方案。我最初认为这将是我的选择,但出于两个原因而决定不采用:
- 实际上,它会打开端口以便进行测试。由于我过去曾因不正确使用
HttpClient
而必须修复端口用尽问题,所以我决定放弃这个解决方案,因为我不确定它在许多并行运行的单元测试中是否能很好地扩展到一个大型代码库中。
- 使用的 URL 必须是可解析的(实际合法的URL)。如果你希望简化不关心“真实” URL 的情况(只要你期望的 URL 被实际调用即可),那么这可能不适合您。
示例
给定以下简单/人为构造的代码,下面是如何编写每个测试的方法。
public class ClassUnderTest
{
private readonly HttpClient _httpClient;
private const string Url = "https://myurl";
public ClassUnderTest(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<Person> GetPersonAsync(int id)
{
var response = await _httpClient.GetAsync($"{Url}?id={id}");
return await response.Content.ReadFromJsonAsync<Person>();
}
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
Moq (by itself)
[Fact]
public async Task JustMoq()
{
const int personId = 1;
var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = JsonContent.Create<Person>(dto)
};
mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
var httpClient = new HttpClient(mockHandler.Object);
var sut = new ClassUnderTest(httpClient);
var actual = await sut.GetPersonAsync(personId);
Assert.NotNull(actual);
mockHandler.Protected().Verify(
"SendAsync",
Times.Exactly(1),
ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
ItExpr.IsAny<CancellationToken>());
}
RichardSzalay.MockHttp(使用后端定义模式)
[Fact]
public async Task RichardSzalayMockHttpUsingBackendDefinition()
{
const int personId = 1;
using var mockHandler = new MockHttpMessageHandler();
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = JsonContent.Create<Person>(dto)
};
var mockedRequest = mockHandler.When(HttpMethod.Get, "https://myurl?id=1")
.Respond(mockResponse.StatusCode, mockResponse.Content);
var httpClient = mockHandler.ToHttpClient();
var sut = new ClassUnderTest(httpClient);
var actual = await sut.GetPersonAsync(personId);
Assert.NotNull(actual);
Assert.Equivalent(dto, actual);
Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
mockHandler.VerifyNoOutstandingRequest();
}
RichardSzalay.MockHttp(使用RequestExpectation模式)
[Fact]
public async Task RichardSzalayMockHttpUsingRequestExpectation()
{
const int personId = 1;
using var mockHandler = new MockHttpMessageHandler();
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockResponse = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = JsonContent.Create<Person>(dto)
};
var mockedRequest = mockHandler.Expect(HttpMethod.Get, "https://myurl")
.WithExactQueryString($"id={personId}")
.Respond(mockResponse.StatusCode, mockResponse.Content);
var httpClient = mockHandler.ToHttpClient();
var sut = new ClassUnderTest(httpClient);
var actual = await sut.GetPersonAsync(personId);
Assert.NotNull(actual);
Assert.Equivalent(dto, actual);
Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
mockHandler.VerifyNoOutstandingExpectation();
}
Moq.Contrib.HttpClient
[Fact]
public async Task UsingMoqContribHttpClient()
{
const int personId = 1;
var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockUrl = $"https://myurl?id={personId}";
var mockResponse = mockHandler.SetupRequest(HttpMethod.Get, mockUrl)
.ReturnsJsonResponse<Person>(HttpStatusCode.OK, dto);
var httpClient = mockHandler.CreateClient();
var sut = new ClassUnderTest(httpClient);
var actual = await sut.GetPersonAsync(personId);
Assert.NotNull(actual);
Assert.Equivalent(dto, actual);
mockHandler.VerifyRequest(HttpMethod.Get, mockUrl, Times.Once());
}
WireMock.NET
public class TestClass : IDisposable
{
private WireMockServer _server;
public TestClass()
{
_server = WireMockServer.Start();
}
public void Dispose()
{
_server.Stop();
}
[Fact]
public async Task UsingWireMock()
{
const int personId = 1;
var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
var mockUrl = $"https://myurl?id={personId}";
_server.Given(
Request.Create()
.WithPath("/"))
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBodyAsJson(dto));
var httpClient = _server.CreateClient();
var sut = new ClassUnderTest(httpClient);
var actual = await sut.GetPersonAsync(personId);
Assert.NotNull(actual);
Assert.Equivalent(dto, actual);
}
}
HttpClient
是有问题的。这会强制客户端使用HttpClient
具体类。相反,你应该暴露一个HttpClient
的 抽象。 - Mike Eason