在单元测试中模拟 HttpClient
Mocking HttpClient in unit tests
我在尝试包装我的代码以便在单元测试中使用时遇到了一些问题。问题是这样的。我有接口 IHttpHandler
:
public interface IHttpHandler
{
HttpClient client { get; }
}
并且 class 使用它,HttpHandler
:
public class HttpHandler : IHttpHandler
{
public HttpClient client
{
get
{
return new HttpClient();
}
}
}
然后是 Connection
class,它使用 simpleIOC 注入客户端实现:
public class Connection
{
private IHttpHandler _httpClient;
public Connection(IHttpHandler httpClient)
{
_httpClient = httpClient;
}
}
然后我有一个单元测试项目 class:
private IHttpHandler _httpClient;
[TestMethod]
public void TestMockConnection()
{
var client = new Connection(_httpClient);
client.doSomething();
// Here I want to somehow create a mock instance of the http client
// Instead of the real one. How Should I approach this?
}
现在显然我将在 Connection
class 中使用方法从我的后端检索数据 (JSON)。但是,我想为此 class 编写单元测试,显然我不想针对真实后端编写测试,而是针对模拟后端编写测试。我已经尝试 google 对此做出很好的回答,但没有取得很大的成功。我可以并且曾经使用过 Moq 来模拟,但从来没有像 HttpClient
这样的东西。我应该如何解决这个问题?
你的接口公开了具体的 HttpClient
class,因此任何使用这个接口的 classes 都绑定到它,这意味着它不能被模拟。
HttpClient
不继承任何接口,因此您必须自己编写。我建议使用 decorator-like 模式:
public interface IHttpHandler
{
HttpResponseMessage Get(string url);
HttpResponseMessage Post(string url, HttpContent content);
Task<HttpResponseMessage> GetAsync(string url);
Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}
而您的 class 将如下所示:
public class HttpClientHandler : IHttpHandler
{
private HttpClient _client = new HttpClient();
public HttpResponseMessage Get(string url)
{
return GetAsync(url).Result;
}
public HttpResponseMessage Post(string url, HttpContent content)
{
return PostAsync(url, content).Result;
}
public async Task<HttpResponseMessage> GetAsync(string url)
{
return await _client.GetAsync(url);
}
public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
{
return await _client.PostAsync(url, content);
}
}
所有这一切的关键在于 HttpClientHandler
创建了自己的 HttpClient
,然后您当然可以创建多个 class 以不同方式实现 IHttpHandler
.
这种方法的主要问题是,您正在有效地编写一个 class,它只调用另一个 class 中的方法,但是您可以创建一个 class 从 HttpClient
继承(参见 Nkosi 的示例,这是比我的方法好得多的方法)。如果 HttpClient
有一个你可以模拟的界面,生活会容易得多,不幸的是它没有。
不过这个例子不是金票。 IHttpHandler
仍然依赖于属于 System.Net.Http
命名空间的 HttpResponseMessage
,因此如果您确实需要 HttpClient
以外的其他实现,则必须执行某种映射来转换他们对 HttpResponseMessage
个对象的回应。这当然只是一个问题如果你需要使用 IHttpHandler
的多个实现 但它看起来不像你这样做所以这不是世界末日,但它是需要考虑的事情。
无论如何,你可以简单地模拟 IHttpHandler
而不必担心具体的 HttpClient
class 因为它已经被抽象掉了。
我建议测试 非异步 方法,因为这些方法仍然调用异步方法,但不必担心单元测试异步方法的麻烦,请参阅 here
如评论中所述,您需要 抽象 离开 HttpClient
以免与其耦合。我过去做过类似的事情。我会尝试调整我所做的与您正在尝试做的事情。
首先查看 HttpClient
class 并确定它提供了哪些功能是需要的。
这里有一个可能性:
public interface IHttpClient {
System.Threading.Tasks.Task<T> DeleteAsync<T>(string uri) where T : class;
System.Threading.Tasks.Task<T> DeleteAsync<T>(Uri uri) where T : class;
System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
System.Threading.Tasks.Task<T> PostAsync<T>(string uri, object package);
System.Threading.Tasks.Task<T> PostAsync<T>(Uri uri, object package);
System.Threading.Tasks.Task<T> PutAsync<T>(string uri, object package);
System.Threading.Tasks.Task<T> PutAsync<T>(Uri uri, object package);
}
再次如前所述,这是出于特定目的。我完全抽象掉了对任何处理 HttpClient
的依赖,并专注于我想要返回的内容。您应该评估如何抽象 HttpClient
以仅提供您想要的必要功能。
这将允许您仅模拟需要测试的内容。
我什至建议完全取消 IHttpHandler
并使用 HttpClient
抽象 IHttpClient
。但我只是不选择,因为您可以用抽象客户端的成员替换处理程序接口的主体。
然后 IHttpClient
的实现可以用于 wrapp/adapt real/concrete HttpClient
或与此相关的任何其他对象,可用于制作 HTTP requests 因为你真正想要的是一种服务,它提供了与 HttpClient
具体相关的功能。使用抽象是一种干净(我的意见)和 SOLID 方法,如果您需要在框架更改时为其他东西切换底层客户端,可以使您的代码更易于维护。
下面是如何完成实施的片段。
/// <summary>
/// HTTP Client adaptor wraps a <see cref="System.Net.Http.HttpClient"/>
/// that contains a reference to <see cref="ConfigurableMessageHandler"/>
/// </summary>
public sealed class HttpClientAdaptor : IHttpClient {
HttpClient httpClient;
public HttpClientAdaptor(IHttpClientFactory httpClientFactory) {
httpClient = httpClientFactory.CreateHttpClient(**Custom configurations**);
}
//...other code
/// <summary>
/// Send a GET request to the specified Uri as an asynchronous operation.
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="uri">The Uri the request is sent to</param>
/// <returns></returns>
public async System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class {
var result = default(T);
//Try to get content as T
try {
//send request and get the response
var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
//if there is content in response to deserialize
if (response.Content.Headers.ContentLength.GetValueOrDefault() > 0) {
//get the content
string responseBodyAsText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
//desrialize it
result = deserializeJsonToObject<T>(responseBodyAsText);
}
} catch (Exception ex) {
Log.Error(ex);
}
return result;
}
//...other code
}
正如您在上面的示例中看到的,许多通常与使用 HttpClient
相关的繁重工作隐藏在抽象背后。
您的连接 class 然后可以注入抽象客户端
public class Connection
{
private IHttpClient _httpClient;
public Connection(IHttpClient httpClient)
{
_httpClient = httpClient;
}
}
然后您的测试可以模拟您的 SUT 所需的内容
private IHttpClient _httpClient;
[TestMethod]
public void TestMockConnection()
{
SomeModelObject model = new SomeModelObject();
var httpClientMock = new Mock<IHttpClient>();
httpClientMock.Setup(c => c.GetAsync<SomeModelObject>(It.IsAny<string>()))
.Returns(() => Task.FromResult(model));
_httpClient = httpClientMock.Object;
var client = new Connection(_httpClient);
// Assuming doSomething uses the client to make
// a request for a model of type SomeModelObject
client.doSomething();
}
HttpClient 的可扩展性在于传递给构造函数的HttpMessageHandler
。它的目的是允许特定于平台的实现,但您也可以模拟它。无需为 HttpClient 创建装饰器包装器。
如果您更喜欢 DSL 而不是使用 Moq,我在 GitHub/Nuget 上有一个库可以让事情变得更简单:https://github.com/richardszalay/mockhttp
Nuget Package RichardSzalay.MockHttp is available here.
var mockHttp = new MockHttpMessageHandler();
// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localost/api/user/*")
.Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON
// Inject the handler or client into your application code
var client = new HttpClient(mockHttp);
var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;
var json = await response.Content.ReadAsStringAsync();
// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}
这是一个常见问题,我非常希望能够模拟 HttpClient,但我想我终于意识到您不应该模拟 HttpClient。这样做似乎合乎逻辑,但我认为我们已经被我们在开源库中看到的东西洗脑了。
我们经常看到 "Clients" 我们在代码中模拟,以便我们可以隔离测试,因此我们自动尝试将相同的原则应用于 HttpClient。 HttpClient 实际上做了很多;您可以将其视为 HttpMessageHandler 的管理器,因此您不想模拟它,这就是它 still 没有接口的原因。对于单元测试或设计服务,您真正感兴趣的部分甚至是 HttpMessageHandler,因为那是 returns 响应,您 可以 模拟它.
还值得指出的是,您可能应该开始将 HttpClient 视为一个更大的交易。例如:将新 HttpClient 的实例化保持在最低限度。重复使用它们,它们被设计为可以重复使用,如果你这样做的话,可以使用更少的资源。如果你开始把它当作一件大事来对待,那么想要模拟它就会感觉更不对劲,现在消息处理程序将开始成为你要注入的东西,而不是客户端。
换句话说,围绕处理程序而不是客户端设计依赖项。更好的是,使用 HttpClient 的抽象 "services" 允许您注入处理程序,并将其用作您的可注入依赖项。然后在您的测试中,您可以伪造处理程序来控制设置测试的响应。
包装 HttpClient 简直是在浪费时间。
更新:
请参阅 Joshua Dooms 的示例。这正是我推荐的。
基于其他答案,我建议使用这段代码,它没有任何外部依赖项:
[TestClass]
public class MyTestClass
{
[TestMethod]
public async Task MyTestMethod()
{
var httpClient = new HttpClient(new MockHttpMessageHandler());
var content = await httpClient.GetStringAsync("http://some.fake.url");
Assert.AreEqual("Content as string", content);
}
}
public class MockHttpMessageHandler : HttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("Content as string")
};
return await Task.FromResult(responseMessage);
}
}
我认为问题是你把它弄得有点颠倒了。
public class AuroraClient : IAuroraClient
{
private readonly HttpClient _client;
public AuroraClient() : this(new HttpClientHandler())
{
}
public AuroraClient(HttpMessageHandler messageHandler)
{
_client = new HttpClient(messageHandler);
}
}
如果你看看上面的class,我想这就是你想要的。 Microsoft 建议让客户端保持活动状态以获得最佳性能,因此这种类型的结构允许您这样做。此外,HttpMessageHandler 是一个抽象 class,因此是可模拟的。您的测试方法将如下所示:
[TestMethod]
public void TestMethod1()
{
// Arrange
var mockMessageHandler = new Mock<HttpMessageHandler>();
// Set up your mock behavior here
var auroraClient = new AuroraClient(mockMessageHandler.Object);
// Act
// Assert
}
这允许您在模拟 HttpClient 的行为时测试您的逻辑。
抱歉各位,在编写并亲自尝试之后,我意识到您无法模拟 HttpMessageHandler 上的受保护方法。我随后添加了以下代码以允许注入适当的模拟。
public interface IMockHttpMessageHandler
{
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}
public class MockHttpMessageHandler : HttpMessageHandler
{
private readonly IMockHttpMessageHandler _realMockHandler;
public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler)
{
_realMockHandler = realMockHandler;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _realMockHandler.SendAsync(request, cancellationToken);
}
}
用它编写的测试看起来像下面这样:
[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
// Arrange
var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
// Set up Mock behavior here.
var client = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
// Act
// Assert
}
我同意其他一些答案,最好的方法是在 HttpClient 中模拟 HttpMessageHandler,而不是包装 HttpClient。这个答案的独特之处在于它仍然注入 HttpClient,允许它是一个单例或通过依赖注入进行管理。
HttpClient is intended to be instantiated once and re-used throughout
the life of an application.
(Source).
模拟 HttpMessageHandler 可能有点棘手,因为 SendAsync 受到保护。这是一个完整的示例,使用 xunit 和 Moq。
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;
// Use nuget to install xunit and Moq
namespace MockHttpClient {
class Program {
static void Main(string[] args) {
var analyzer = new SiteAnalyzer(Client);
var size = analyzer.GetContentSize("http://microsoft.com").Result;
Console.WriteLine($"Size: {size}");
}
private static readonly HttpClient Client = new HttpClient(); // Singleton
}
public class SiteAnalyzer {
public SiteAnalyzer(HttpClient httpClient) {
_httpClient = httpClient;
}
public async Task<int> GetContentSize(string uri)
{
var response = await _httpClient.GetAsync( uri );
var content = await response.Content.ReadAsStringAsync();
return content.Length;
}
private readonly HttpClient _httpClient;
}
public class SiteAnalyzerTests {
[Fact]
public async void GetContentSizeReturnsCorrectLength() {
// Arrange
const string testContent = "test content";
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(testContent)
});
var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object));
// Act
var result = await underTest.GetContentSize("http://anyurl");
// Assert
Assert.Equal(testContent.Length, result);
}
}
}
一种替代方法是设置一个存根 HTTP 服务器,该服务器 returns 根据与请求 url 匹配的模式来封装响应,这意味着您测试的是真实的 HTTP 请求,而不是模拟请求。从历史上看,这会花费大量的开发工作,并且考虑进行单元测试会慢得多,但是 OSS 库 WireMock.net 易于使用并且速度足够快 运行 进行大量测试,因此可能值得考虑。设置是几行代码:
var server = FluentMockServer.Start();
server.Given(
Request.Create()
.WithPath("/some/thing").UsingGet()
)
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBody("{'attr':'value'}")
);
您可以找到更多详细信息,guidance on using wiremock in tests here.
我的一位同事注意到大多数 HttpClient
方法都在幕后调用 SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
,这是 HttpMessageInvoker
:
的虚方法
到目前为止,模拟 HttpClient
的最简单方法是简单地模拟该特定方法:
var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);
并且您的代码可以调用大多数(但不是全部)HttpClient
class 方法,包括常规
httpClient.SendAsync(req)
点击这里确认
https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs
这是一个老问题,但我很想用我在这里没有看到的解决方案来扩展答案。
您可以伪造 Microsoft 程序集 (System.Net.Http),然后在测试期间使用 ShinsContext。
- 在 VS 2017 中,右键单击 System.Net.Http 程序集并选择 "Add Fakes Assembly"
- 将您的代码放在 ShimsContext.Create() using 下的单元测试方法中。这样,您可以隔离您计划伪造 HttpClient 的代码。
取决于您的实施和测试,我建议在您调用 HttpClient 上的方法并想要伪造返回值的地方实施所有所需的操作。使用 ShimHttpClient.AllInstances 将在测试期间创建的所有实例中伪造您的实现。例如,如果您想伪造 GetAsync() 方法,请执行以下操作:
[TestMethod]
public void FakeHttpClient()
{
using (ShimsContext.Create())
{
System.Net.Http.Fakes.ShimHttpClient.AllInstances.GetAsyncString = (c, requestUri) =>
{
//Return a service unavailable response
var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);
var task = Task.FromResult(httpResponseMessage);
return task;
};
//your implementation will use the fake method(s) automatically
var client = new Connection(_httpClient);
client.doSomething();
}
}
我做了一些非常简单的事情,因为我在 DI 环境中。
public class HttpHelper : IHttpHelper
{
private ILogHelper _logHelper;
public HttpHelper(ILogHelper logHelper)
{
_logHelper = logHelper;
}
public virtual async Task<HttpResponseMessage> GetAsync(string uri, Dictionary<string, string> headers = null)
{
HttpResponseMessage response;
using (var client = new HttpClient())
{
if (headers != null)
{
foreach (var h in headers)
{
client.DefaultRequestHeaders.Add(h.Key, h.Value);
}
}
response = await client.GetAsync(uri);
}
return response;
}
public async Task<T> GetAsync<T>(string uri, Dictionary<string, string> headers = null)
{
...
rawResponse = await GetAsync(uri, headers);
...
}
}
模拟是:
[TestInitialize]
public void Initialize()
{
...
_httpHelper = new Mock<HttpHelper>(_logHelper.Object) { CallBase = true };
...
}
[TestMethod]
public async Task SuccessStatusCode_WithAuthHeader()
{
...
_httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns(
Task<HttpResponseMessage>.Factory.StartNew(() =>
{
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(_testData))
};
})
);
var result = await _httpHelper.Object.GetAsync<TestDTO>(...);
Assert.AreEqual(...);
}
很多答案我都不信服。
首先,假设您要对使用 HttpClient
的方法进行单元测试。您不应该在您的实现中直接实例化 HttpClient
。您应该注入一个工厂,负责为您提供 HttpClient
的实例。这样你就可以稍后在那个工厂上模拟 return 你想要的 HttpClient
(例如:模拟 HttpClient
而不是真实的)。
因此,您将拥有一个如下所示的工厂:
public interface IHttpClientFactory
{
HttpClient Create();
}
和一个实现:
public class HttpClientFactory
: IHttpClientFactory
{
public HttpClient Create()
{
var httpClient = new HttpClient();
return httpClient;
}
}
当然你需要在你的 IoC 容器中注册这个实现。如果您使用 Autofac,它将类似于:
builder
.RegisterType<IHttpClientFactory>()
.As<HttpClientFactory>()
.SingleInstance();
现在您将拥有一个正确且可测试的实现。假设您的方法类似于:
public class MyHttpClient
: IMyHttpClient
{
private readonly IHttpClientFactory _httpClientFactory;
public SalesOrderHttpClient(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<string> PostAsync(Uri uri, string content)
{
using (var client = _httpClientFactory.Create())
{
var clientAddress = uri.GetLeftPart(UriPartial.Authority);
client.BaseAddress = new Uri(clientAddress);
var content = new StringContent(content, Encoding.UTF8, "application/json");
var uriAbsolutePath = uri.AbsolutePath;
var response = await client.PostAsync(uriAbsolutePath, content);
var responseJson = response.Content.ReadAsStringAsync().Result;
return responseJson;
}
}
}
现在是测试部分。 HttpClient
扩展了 HttpMessageHandler
,这是抽象的。让我们创建一个接受委托的 HttpMessageHandler
的“模拟”,这样当我们使用模拟时,我们还可以为每个测试设置每个行为。
public class MockHttpMessageHandler
: HttpMessageHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;
public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
{
_sendAsyncFunc = sendAsyncFunc;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _sendAsyncFunc.Invoke(request, cancellationToken);
}
}
现在,在 Moq(和 FluentAssertions,一个使单元测试更具可读性的库)的帮助下,我们拥有了对使用 HttpClient
的方法 PostAsync 进行单元测试所需的一切
public static class PostAsyncTests
{
public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
: Given_WhenAsync_Then_Test
{
private SalesOrderHttpClient _sut;
private Uri _uri;
private string _content;
private string _expectedResult;
private string _result;
protected override void Given()
{
_uri = new Uri("http://test.com/api/resources");
_content = "{\"foo\": \"bar\"}";
_expectedResult = "{\"result\": \"ok\"}";
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var messageHandlerMock =
new MockHttpMessageHandler((request, cancellation) =>
{
var responseMessage =
new HttpResponseMessage(HttpStatusCode.Created)
{
Content = new StringContent("{\"result\": \"ok\"}")
};
var result = Task.FromResult(responseMessage);
return result;
});
var httpClient = new HttpClient(messageHandlerMock);
httpClientFactoryMock
.Setup(x => x.Create())
.Returns(httpClient);
var httpClientFactory = httpClientFactoryMock.Object;
_sut = new SalesOrderHttpClient(httpClientFactory);
}
protected override async Task WhenAsync()
{
_result = await _sut.PostAsync(_uri, _content);
}
[Fact]
public void Then_It_Should_Return_A_Valid_JsonMessage()
{
_result.Should().BeEquivalentTo(_expectedResult);
}
}
}
显然这个测试很愚蠢,我们真的在测试我们的模拟。但是你明白了。您应该根据您的实现来测试有意义的逻辑,例如..
- 如果响应的代码状态不是201,是否应该抛出异常?
- 如果无法解析响应文本,应该怎么办?
- 等等
这个答案的目的是测试使用 HttpClient 的东西,这是一种非常干净的方法。
更新
最近我在我的测试中使用了一个 http 构建器,我可以在其中轻松地注入我期望的 json 响应。
public class HttpClientBuilder
{
private HttpMessageHandler _httpMessageHandler = new HttpClientHandler();
public HttpClientBuilder WithJsonResponse(HttpStatusCode httpStatusCode, string json, string contentType = "application/json")
{
var mockHttpMessageHandler =
new MockHttpMessageHandler(
(request, cancellation) =>
{
var responseMessage =
new HttpResponseMessage(httpStatusCode)
{
Content = new StringContent(json, Encoding.UTF8, contentType)
};
var result = Task.FromResult(responseMessage);
return result;
});
_httpMessageHandler = mockHttpMessageHandler;
return this;
}
public HttpClient Build()
{
var httpClient = new HttpClient(_httpMessageHandler);
return httpClient;
}
}
class MockHttpMessageHandler
: HttpMessageHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;
public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
{
_sendAsyncFunc = sendAsyncFunc;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _sendAsyncFunc.Invoke(request, cancellationToken);
}
}
所以,只要我有像 IHttpClientFactory
这样的抽象背后的 HttpClient,正如我上面所建议的,在我的测试中我可以做一些像
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var jsonResponse = "{\"hello world\"}";
var httpClient =
new HttpClientBuilder()
.WithJsonResponse(HttpStatusCode.OK, jsonResponse)
.Build();
httpClientFactoryMock
.Setup(x => x.Create())
.Returns(httpClient);
var httpClientFactory = httpClientFactoryMock.Object;
然后使用那个 httpClientFactory。
您所需要的只是 HttpMessageHandler
class 的测试版本,您将其传递给 HttpClient
ctor。要点是您的测试 HttpMessageHandler
class 将有一个 HttpRequestHandler
委托,调用者可以设置它并简单地按照他们想要的方式处理 HttpRequest
。
public class FakeHttpMessageHandler : HttpMessageHandler
{
public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
(r, c) =>
new HttpResponseMessage
{
ReasonPhrase = r.RequestUri.AbsoluteUri,
StatusCode = HttpStatusCode.OK
};
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(HttpRequestHandler(request, cancellationToken));
}
}
您可以使用此 class 的实例来创建具体的 HttpClient 实例。通过 HttpRequestHandler 委托,您可以完全控制来自 HttpClient 的传出 http 请求。
加入聚会有点晚,但我喜欢在具有下游 REST 依赖项的 dotnet 核心微服务的集成测试中尽可能使用 wiremocking (https://github.com/WireMock-Net/WireMock.Net)。
通过实现扩展 IHttpClientFactory 的 TestHttpClientFactory,我们可以覆盖方法
HttpClient CreateClient(字符串名称)
因此,当在您的应用程序中使用命名客户端时,您可以控制 return 连接到您的 Wiremock 的 HttpClient。
这种方法的好处是您不会在您正在测试的应用程序中更改任何内容,并启用课程集成测试对您的服务执行实际的 REST 请求并模拟 json(或其他)实际的下游请求应该 return。这会导致在您的应用程序中进行简洁的测试和尽可能少的模拟。
public class TestHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
var httpClient = new HttpClient
{
BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
// G.Config is our singleton config access, so the endpoint
// to the running wiremock is used in the test
};
return httpClient;
}
}
和
// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);
灵感来自 , here's a sample using NUnit and FakeItEasy。
本例中的 SystemUnderTest
是您要测试的 class - 没有给出示例内容,但我假设您已经有了!
[TestFixture]
public class HttpClientTests
{
private ISystemUnderTest _systemUnderTest;
private HttpMessageHandler _mockMessageHandler;
[SetUp]
public void Setup()
{
_mockMessageHandler = A.Fake<HttpMessageHandler>();
var httpClient = new HttpClient(_mockMessageHandler);
_systemUnderTest = new SystemUnderTest(httpClient);
}
[Test]
public void HttpError()
{
// Arrange
A.CallTo(_mockMessageHandler)
.Where(x => x.Method.Name == "SendAsync")
.WithReturnType<Task<HttpResponseMessage>>()
.Returns(Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.InternalServerError,
Content = new StringContent("abcd")
}));
// Act
var result = _systemUnderTest.DoSomething();
// Assert
// Assert.AreEqual(...);
}
}
这是一个简单的解决方案,对我来说效果很好。
使用最小起订量模拟库。
// ARRANGE
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock
.Protected()
// Setup the PROTECTED method to mock
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
// prepare the expected response of the mocked http call
.ReturnsAsync(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("[{'id':1,'value':'1'}]"),
})
.Verifiable();
// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
BaseAddress = new Uri("http://test.com/"),
};
var subjectUnderTest = new MyTestClass(httpClient);
// ACT
var result = await subjectUnderTest
.GetSomethingRemoteAsync('api/test/whatever');
// ASSERT
result.Should().NotBeNull(); // this is fluent assertions here...
result.Id.Should().Be(1);
// also check the 'http' call was like we expected it
var expectedUri = new Uri("http://test.com/api/test/whatever");
handlerMock.Protected().Verify(
"SendAsync",
Times.Exactly(1), // we expected a single external request
ItExpr.Is<HttpRequestMessage>(req =>
req.Method == HttpMethod.Get // we expected a GET request
&& req.RequestUri == expectedUri // to this uri
),
ItExpr.IsAny<CancellationToken>()
);
来源:https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/
也许您当前的项目中需要更改一些代码,但对于新项目,您绝对应该考虑使用 Flurl。
它是 .NET 的 HTTP 客户端库,具有流畅的界面,专门为使用它发出 HTTP 请求的代码启用可测试性。
网站上有很多代码示例,但简而言之,您可以在代码中像这样使用它。
添加用法。
using Flurl;
using Flurl.Http;
发送获取请求并读取响应。
public async Task SendGetRequest()
{
var response = await "https://example.com".GetAsync();
// ...
}
在单元测试中,Flurl 充当模拟,可以配置为按需要运行,还可以验证已完成的调用。
using (var httpTest = new HttpTest())
{
// Arrange
httpTest.RespondWith("OK", 200);
// Act
await sut.SendGetRequest();
// Assert
httpTest.ShouldHaveCalled("https://example.com")
.WithVerb(HttpMethod.Get);
}
由于HttpClient
使用SendAsync
方法执行所有HTTP Requests
,您可以override SendAsync
方法并模拟HttpClient
。
对于创建 HttpClient
到 interface
的包装,如下所示
public interface IServiceHelper
{
HttpClient GetClient();
}
然后使用上面 interface
在您的服务中进行依赖注入,示例如下
public class SampleService
{
private readonly IServiceHelper serviceHelper;
public SampleService(IServiceHelper serviceHelper)
{
this.serviceHelper = serviceHelper;
}
public async Task<HttpResponseMessage> Get(int dummyParam)
{
try
{
var dummyUrl = "http://www.dummyurl.com/api/controller/" + dummyParam;
var client = serviceHelper.GetClient();
HttpResponseMessage response = await client.GetAsync(dummyUrl);
return response;
}
catch (Exception)
{
// log.
throw;
}
}
}
现在在单元测试项目中创建一个助手 class 用于模拟 SendAsync
。
这是一个 FakeHttpResponseHandler
class 也就是 inheriting
DelegatingHandler
它将提供一个选项来覆盖 SendAsync
方法。覆盖 SendAsync
方法后,需要为调用 SendAsync
方法的每个 HTTP Request
设置一个响应,为此创建一个 Dictionary
,key
作为 Uri
和 value
作为 HttpResponseMessage
这样只要有 HTTP Request
并且如果 Uri
匹配 SendAsync
将 return 配置的 HttpResponseMessage
.
public class FakeHttpResponseHandler : DelegatingHandler
{
private readonly IDictionary<Uri, HttpResponseMessage> fakeServiceResponse;
private readonly JavaScriptSerializer javaScriptSerializer;
public FakeHttpResponseHandler()
{
fakeServiceResponse = new Dictionary<Uri, HttpResponseMessage>();
javaScriptSerializer = new JavaScriptSerializer();
}
/// <summary>
/// Used for adding fake httpResponseMessage for the httpClient operation.
/// </summary>
/// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
/// <param name="uri">Service end point URL.</param>
/// <param name="httpResponseMessage"> Response expected when the service called.</param>
public void AddFakeServiceResponse(Uri uri, HttpResponseMessage httpResponseMessage)
{
fakeServiceResponse.Remove(uri);
fakeServiceResponse.Add(uri, httpResponseMessage);
}
/// <summary>
/// Used for adding fake httpResponseMessage for the httpClient operation having query string parameter.
/// </summary>
/// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
/// <param name="uri">Service end point URL.</param>
/// <param name="httpResponseMessage"> Response expected when the service called.</param>
/// <param name="requestParameter">Query string parameter.</param>
public void AddFakeServiceResponse<TQueryStringParameter>(Uri uri, HttpResponseMessage httpResponseMessage, TQueryStringParameter requestParameter)
{
var serilizedQueryStringParameter = javaScriptSerializer.Serialize(requestParameter);
var actualUri = new Uri(string.Concat(uri, serilizedQueryStringParameter));
fakeServiceResponse.Remove(actualUri);
fakeServiceResponse.Add(actualUri, httpResponseMessage);
}
// all method in HttpClient call use SendAsync method internally so we are overriding that method here.
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if(fakeServiceResponse.ContainsKey(request.RequestUri))
{
return Task.FromResult(fakeServiceResponse[request.RequestUri]);
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
{
RequestMessage = request,
Content = new StringContent("Not matching fake found")
});
}
}
通过模拟框架或类似下面的方法为 IServiceHelper
创建一个新的实现。
这个 FakeServiceHelper
class 我们可以用来注入 FakeHttpResponseHandler
class 这样每当这个 class
创建的 HttpClient
它将使用 FakeHttpResponseHandler class
而不是实际的实现。
public class FakeServiceHelper : IServiceHelper
{
private readonly DelegatingHandler delegatingHandler;
public FakeServiceHelper(DelegatingHandler delegatingHandler)
{
this.delegatingHandler = delegatingHandler;
}
public HttpClient GetClient()
{
return new HttpClient(delegatingHandler);
}
}
并在测试中通过添加 Uri
和预期的 HttpResponseMessage
配置 FakeHttpResponseHandler class
。
Uri
应该是实际的 service
端点 Uri
以便当从实际 service
实现中调用 overridden SendAsync
方法时它将匹配 Uri
在 Dictionary
中并使用配置的 HttpResponseMessage
进行响应。
配置后将 FakeHttpResponseHandler object
注入到伪造的 IServiceHelper
实现中。
然后将 FakeServiceHelper class
注入实际服务,这将使实际服务使用 override SendAsync
方法。
[TestClass]
public class SampleServiceTest
{
private FakeHttpResponseHandler fakeHttpResponseHandler;
[TestInitialize]
public void Initialize()
{
fakeHttpResponseHandler = new FakeHttpResponseHandler();
}
[TestMethod]
public async Task GetMethodShouldReturnFakeResponse()
{
Uri uri = new Uri("http://www.dummyurl.com/api/controller/");
const int dummyParam = 123456;
const string expectdBody = "Expected Response";
var expectedHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(expectdBody)
};
fakeHttpResponseHandler.AddFakeServiceResponse(uri, expectedHttpResponseMessage, dummyParam);
var fakeServiceHelper = new FakeServiceHelper(fakeHttpResponseHandler);
var sut = new SampleService(fakeServiceHelper);
var response = await sut.Get(dummyParam);
var responseBody = await response.Content.ReadAsStringAsync();
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.AreEqual(expectdBody, responseBody);
}
}
经过仔细搜索,我找到了实现此目的的最佳方法。
private HttpResponseMessage response;
[SetUp]
public void Setup()
{
var handlerMock = new Mock<HttpMessageHandler>();
handlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
// This line will let you to change the response in each test method
.ReturnsAsync(() => response);
_httpClient = new HttpClient(handlerMock.Object);
yourClinet = new YourClient( _httpClient);
}
As you noticed I have used Moq and Moq.Protected packages.
添加我的 2 美分。模拟特定的 http 请求方法 Get 或 Post。这对我有用。
mockHttpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(a => a.Method == HttpMethod.Get), ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(""),
})).Verifiable();
Microsoft 现在建议使用 IHttpClientFactory
而不是直接使用 HttpClient
:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0
请求 returns 预期结果的模拟示例:
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;
}
来源:
不要 有一个创建 HttpClient 新实例的包装器。如果这样做,您将 运行 在 运行 时间用完套接字(即使您正在处理 HttpClient 对象)。
如果使用最小起订量,正确的做法是将 using Moq.Protected;
添加到您的测试中,然后编写如下代码:
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("It worked!")
};
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(() => response);
var httpClient = new HttpClient(mockHttpMessageHandler.Object);
如果你不介意运行你自己的http服务器,你可以试试Xim。就这么简单:
using Xim.Simulators.Api;
[Test]
public async Task TestHttpGetMethod()
{
using var simulation = Simulation.Create();
using var api = simulation
.AddApi()
.AddHandler("GET /books/1234", ApiResponse.Ok())
.Build();
await api.StartAsync();
var httpClient = new HttpClient();
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{api.Location}/books/1234"));
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.IsTrue(api.ReceivedApiCalls.Any(call => call.Action == "GET /books/1234"));
}
这是使用 mocks 的一个不错的替代方法,在某些情况下可能会满足您的需求。它建立在 Kestrel 之上(是的,我是作者)。
我在尝试包装我的代码以便在单元测试中使用时遇到了一些问题。问题是这样的。我有接口 IHttpHandler
:
public interface IHttpHandler
{
HttpClient client { get; }
}
并且 class 使用它,HttpHandler
:
public class HttpHandler : IHttpHandler
{
public HttpClient client
{
get
{
return new HttpClient();
}
}
}
然后是 Connection
class,它使用 simpleIOC 注入客户端实现:
public class Connection
{
private IHttpHandler _httpClient;
public Connection(IHttpHandler httpClient)
{
_httpClient = httpClient;
}
}
然后我有一个单元测试项目 class:
private IHttpHandler _httpClient;
[TestMethod]
public void TestMockConnection()
{
var client = new Connection(_httpClient);
client.doSomething();
// Here I want to somehow create a mock instance of the http client
// Instead of the real one. How Should I approach this?
}
现在显然我将在 Connection
class 中使用方法从我的后端检索数据 (JSON)。但是,我想为此 class 编写单元测试,显然我不想针对真实后端编写测试,而是针对模拟后端编写测试。我已经尝试 google 对此做出很好的回答,但没有取得很大的成功。我可以并且曾经使用过 Moq 来模拟,但从来没有像 HttpClient
这样的东西。我应该如何解决这个问题?
你的接口公开了具体的 HttpClient
class,因此任何使用这个接口的 classes 都绑定到它,这意味着它不能被模拟。
HttpClient
不继承任何接口,因此您必须自己编写。我建议使用 decorator-like 模式:
public interface IHttpHandler
{
HttpResponseMessage Get(string url);
HttpResponseMessage Post(string url, HttpContent content);
Task<HttpResponseMessage> GetAsync(string url);
Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}
而您的 class 将如下所示:
public class HttpClientHandler : IHttpHandler
{
private HttpClient _client = new HttpClient();
public HttpResponseMessage Get(string url)
{
return GetAsync(url).Result;
}
public HttpResponseMessage Post(string url, HttpContent content)
{
return PostAsync(url, content).Result;
}
public async Task<HttpResponseMessage> GetAsync(string url)
{
return await _client.GetAsync(url);
}
public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
{
return await _client.PostAsync(url, content);
}
}
所有这一切的关键在于 HttpClientHandler
创建了自己的 HttpClient
,然后您当然可以创建多个 class 以不同方式实现 IHttpHandler
.
这种方法的主要问题是,您正在有效地编写一个 class,它只调用另一个 class 中的方法,但是您可以创建一个 class 从 HttpClient
继承(参见 Nkosi 的示例,这是比我的方法好得多的方法)。如果 HttpClient
有一个你可以模拟的界面,生活会容易得多,不幸的是它没有。
不过这个例子不是金票。 IHttpHandler
仍然依赖于属于 System.Net.Http
命名空间的 HttpResponseMessage
,因此如果您确实需要 HttpClient
以外的其他实现,则必须执行某种映射来转换他们对 HttpResponseMessage
个对象的回应。这当然只是一个问题如果你需要使用 IHttpHandler
的多个实现 但它看起来不像你这样做所以这不是世界末日,但它是需要考虑的事情。
无论如何,你可以简单地模拟 IHttpHandler
而不必担心具体的 HttpClient
class 因为它已经被抽象掉了。
我建议测试 非异步 方法,因为这些方法仍然调用异步方法,但不必担心单元测试异步方法的麻烦,请参阅 here
如评论中所述,您需要 抽象 离开 HttpClient
以免与其耦合。我过去做过类似的事情。我会尝试调整我所做的与您正在尝试做的事情。
首先查看 HttpClient
class 并确定它提供了哪些功能是需要的。
这里有一个可能性:
public interface IHttpClient {
System.Threading.Tasks.Task<T> DeleteAsync<T>(string uri) where T : class;
System.Threading.Tasks.Task<T> DeleteAsync<T>(Uri uri) where T : class;
System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
System.Threading.Tasks.Task<T> PostAsync<T>(string uri, object package);
System.Threading.Tasks.Task<T> PostAsync<T>(Uri uri, object package);
System.Threading.Tasks.Task<T> PutAsync<T>(string uri, object package);
System.Threading.Tasks.Task<T> PutAsync<T>(Uri uri, object package);
}
再次如前所述,这是出于特定目的。我完全抽象掉了对任何处理 HttpClient
的依赖,并专注于我想要返回的内容。您应该评估如何抽象 HttpClient
以仅提供您想要的必要功能。
这将允许您仅模拟需要测试的内容。
我什至建议完全取消 IHttpHandler
并使用 HttpClient
抽象 IHttpClient
。但我只是不选择,因为您可以用抽象客户端的成员替换处理程序接口的主体。
然后 IHttpClient
的实现可以用于 wrapp/adapt real/concrete HttpClient
或与此相关的任何其他对象,可用于制作 HTTP requests 因为你真正想要的是一种服务,它提供了与 HttpClient
具体相关的功能。使用抽象是一种干净(我的意见)和 SOLID 方法,如果您需要在框架更改时为其他东西切换底层客户端,可以使您的代码更易于维护。
下面是如何完成实施的片段。
/// <summary>
/// HTTP Client adaptor wraps a <see cref="System.Net.Http.HttpClient"/>
/// that contains a reference to <see cref="ConfigurableMessageHandler"/>
/// </summary>
public sealed class HttpClientAdaptor : IHttpClient {
HttpClient httpClient;
public HttpClientAdaptor(IHttpClientFactory httpClientFactory) {
httpClient = httpClientFactory.CreateHttpClient(**Custom configurations**);
}
//...other code
/// <summary>
/// Send a GET request to the specified Uri as an asynchronous operation.
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="uri">The Uri the request is sent to</param>
/// <returns></returns>
public async System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class {
var result = default(T);
//Try to get content as T
try {
//send request and get the response
var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
//if there is content in response to deserialize
if (response.Content.Headers.ContentLength.GetValueOrDefault() > 0) {
//get the content
string responseBodyAsText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
//desrialize it
result = deserializeJsonToObject<T>(responseBodyAsText);
}
} catch (Exception ex) {
Log.Error(ex);
}
return result;
}
//...other code
}
正如您在上面的示例中看到的,许多通常与使用 HttpClient
相关的繁重工作隐藏在抽象背后。
您的连接 class 然后可以注入抽象客户端
public class Connection
{
private IHttpClient _httpClient;
public Connection(IHttpClient httpClient)
{
_httpClient = httpClient;
}
}
然后您的测试可以模拟您的 SUT 所需的内容
private IHttpClient _httpClient;
[TestMethod]
public void TestMockConnection()
{
SomeModelObject model = new SomeModelObject();
var httpClientMock = new Mock<IHttpClient>();
httpClientMock.Setup(c => c.GetAsync<SomeModelObject>(It.IsAny<string>()))
.Returns(() => Task.FromResult(model));
_httpClient = httpClientMock.Object;
var client = new Connection(_httpClient);
// Assuming doSomething uses the client to make
// a request for a model of type SomeModelObject
client.doSomething();
}
HttpClient 的可扩展性在于传递给构造函数的HttpMessageHandler
。它的目的是允许特定于平台的实现,但您也可以模拟它。无需为 HttpClient 创建装饰器包装器。
如果您更喜欢 DSL 而不是使用 Moq,我在 GitHub/Nuget 上有一个库可以让事情变得更简单:https://github.com/richardszalay/mockhttp
Nuget Package RichardSzalay.MockHttp is available here.
var mockHttp = new MockHttpMessageHandler();
// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localost/api/user/*")
.Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON
// Inject the handler or client into your application code
var client = new HttpClient(mockHttp);
var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;
var json = await response.Content.ReadAsStringAsync();
// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}
这是一个常见问题,我非常希望能够模拟 HttpClient,但我想我终于意识到您不应该模拟 HttpClient。这样做似乎合乎逻辑,但我认为我们已经被我们在开源库中看到的东西洗脑了。
我们经常看到 "Clients" 我们在代码中模拟,以便我们可以隔离测试,因此我们自动尝试将相同的原则应用于 HttpClient。 HttpClient 实际上做了很多;您可以将其视为 HttpMessageHandler 的管理器,因此您不想模拟它,这就是它 still 没有接口的原因。对于单元测试或设计服务,您真正感兴趣的部分甚至是 HttpMessageHandler,因为那是 returns 响应,您 可以 模拟它.
还值得指出的是,您可能应该开始将 HttpClient 视为一个更大的交易。例如:将新 HttpClient 的实例化保持在最低限度。重复使用它们,它们被设计为可以重复使用,如果你这样做的话,可以使用更少的资源。如果你开始把它当作一件大事来对待,那么想要模拟它就会感觉更不对劲,现在消息处理程序将开始成为你要注入的东西,而不是客户端。
换句话说,围绕处理程序而不是客户端设计依赖项。更好的是,使用 HttpClient 的抽象 "services" 允许您注入处理程序,并将其用作您的可注入依赖项。然后在您的测试中,您可以伪造处理程序来控制设置测试的响应。
包装 HttpClient 简直是在浪费时间。
更新: 请参阅 Joshua Dooms 的示例。这正是我推荐的。
基于其他答案,我建议使用这段代码,它没有任何外部依赖项:
[TestClass]
public class MyTestClass
{
[TestMethod]
public async Task MyTestMethod()
{
var httpClient = new HttpClient(new MockHttpMessageHandler());
var content = await httpClient.GetStringAsync("http://some.fake.url");
Assert.AreEqual("Content as string", content);
}
}
public class MockHttpMessageHandler : HttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("Content as string")
};
return await Task.FromResult(responseMessage);
}
}
我认为问题是你把它弄得有点颠倒了。
public class AuroraClient : IAuroraClient
{
private readonly HttpClient _client;
public AuroraClient() : this(new HttpClientHandler())
{
}
public AuroraClient(HttpMessageHandler messageHandler)
{
_client = new HttpClient(messageHandler);
}
}
如果你看看上面的class,我想这就是你想要的。 Microsoft 建议让客户端保持活动状态以获得最佳性能,因此这种类型的结构允许您这样做。此外,HttpMessageHandler 是一个抽象 class,因此是可模拟的。您的测试方法将如下所示:
[TestMethod]
public void TestMethod1()
{
// Arrange
var mockMessageHandler = new Mock<HttpMessageHandler>();
// Set up your mock behavior here
var auroraClient = new AuroraClient(mockMessageHandler.Object);
// Act
// Assert
}
这允许您在模拟 HttpClient 的行为时测试您的逻辑。
抱歉各位,在编写并亲自尝试之后,我意识到您无法模拟 HttpMessageHandler 上的受保护方法。我随后添加了以下代码以允许注入适当的模拟。
public interface IMockHttpMessageHandler
{
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}
public class MockHttpMessageHandler : HttpMessageHandler
{
private readonly IMockHttpMessageHandler _realMockHandler;
public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler)
{
_realMockHandler = realMockHandler;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _realMockHandler.SendAsync(request, cancellationToken);
}
}
用它编写的测试看起来像下面这样:
[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
// Arrange
var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
// Set up Mock behavior here.
var client = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
// Act
// Assert
}
我同意其他一些答案,最好的方法是在 HttpClient 中模拟 HttpMessageHandler,而不是包装 HttpClient。这个答案的独特之处在于它仍然注入 HttpClient,允许它是一个单例或通过依赖注入进行管理。
HttpClient is intended to be instantiated once and re-used throughout the life of an application.
(Source).
模拟 HttpMessageHandler 可能有点棘手,因为 SendAsync 受到保护。这是一个完整的示例,使用 xunit 和 Moq。
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;
// Use nuget to install xunit and Moq
namespace MockHttpClient {
class Program {
static void Main(string[] args) {
var analyzer = new SiteAnalyzer(Client);
var size = analyzer.GetContentSize("http://microsoft.com").Result;
Console.WriteLine($"Size: {size}");
}
private static readonly HttpClient Client = new HttpClient(); // Singleton
}
public class SiteAnalyzer {
public SiteAnalyzer(HttpClient httpClient) {
_httpClient = httpClient;
}
public async Task<int> GetContentSize(string uri)
{
var response = await _httpClient.GetAsync( uri );
var content = await response.Content.ReadAsStringAsync();
return content.Length;
}
private readonly HttpClient _httpClient;
}
public class SiteAnalyzerTests {
[Fact]
public async void GetContentSizeReturnsCorrectLength() {
// Arrange
const string testContent = "test content";
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(testContent)
});
var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object));
// Act
var result = await underTest.GetContentSize("http://anyurl");
// Assert
Assert.Equal(testContent.Length, result);
}
}
}
一种替代方法是设置一个存根 HTTP 服务器,该服务器 returns 根据与请求 url 匹配的模式来封装响应,这意味着您测试的是真实的 HTTP 请求,而不是模拟请求。从历史上看,这会花费大量的开发工作,并且考虑进行单元测试会慢得多,但是 OSS 库 WireMock.net 易于使用并且速度足够快 运行 进行大量测试,因此可能值得考虑。设置是几行代码:
var server = FluentMockServer.Start();
server.Given(
Request.Create()
.WithPath("/some/thing").UsingGet()
)
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBody("{'attr':'value'}")
);
您可以找到更多详细信息,guidance on using wiremock in tests here.
我的一位同事注意到大多数 HttpClient
方法都在幕后调用 SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
,这是 HttpMessageInvoker
:
到目前为止,模拟 HttpClient
的最简单方法是简单地模拟该特定方法:
var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);
并且您的代码可以调用大多数(但不是全部)HttpClient
class 方法,包括常规
httpClient.SendAsync(req)
点击这里确认 https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs
这是一个老问题,但我很想用我在这里没有看到的解决方案来扩展答案。
您可以伪造 Microsoft 程序集 (System.Net.Http),然后在测试期间使用 ShinsContext。
- 在 VS 2017 中,右键单击 System.Net.Http 程序集并选择 "Add Fakes Assembly"
- 将您的代码放在 ShimsContext.Create() using 下的单元测试方法中。这样,您可以隔离您计划伪造 HttpClient 的代码。
取决于您的实施和测试,我建议在您调用 HttpClient 上的方法并想要伪造返回值的地方实施所有所需的操作。使用 ShimHttpClient.AllInstances 将在测试期间创建的所有实例中伪造您的实现。例如,如果您想伪造 GetAsync() 方法,请执行以下操作:
[TestMethod] public void FakeHttpClient() { using (ShimsContext.Create()) { System.Net.Http.Fakes.ShimHttpClient.AllInstances.GetAsyncString = (c, requestUri) => { //Return a service unavailable response var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable); var task = Task.FromResult(httpResponseMessage); return task; }; //your implementation will use the fake method(s) automatically var client = new Connection(_httpClient); client.doSomething(); } }
我做了一些非常简单的事情,因为我在 DI 环境中。
public class HttpHelper : IHttpHelper
{
private ILogHelper _logHelper;
public HttpHelper(ILogHelper logHelper)
{
_logHelper = logHelper;
}
public virtual async Task<HttpResponseMessage> GetAsync(string uri, Dictionary<string, string> headers = null)
{
HttpResponseMessage response;
using (var client = new HttpClient())
{
if (headers != null)
{
foreach (var h in headers)
{
client.DefaultRequestHeaders.Add(h.Key, h.Value);
}
}
response = await client.GetAsync(uri);
}
return response;
}
public async Task<T> GetAsync<T>(string uri, Dictionary<string, string> headers = null)
{
...
rawResponse = await GetAsync(uri, headers);
...
}
}
模拟是:
[TestInitialize]
public void Initialize()
{
...
_httpHelper = new Mock<HttpHelper>(_logHelper.Object) { CallBase = true };
...
}
[TestMethod]
public async Task SuccessStatusCode_WithAuthHeader()
{
...
_httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns(
Task<HttpResponseMessage>.Factory.StartNew(() =>
{
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(_testData))
};
})
);
var result = await _httpHelper.Object.GetAsync<TestDTO>(...);
Assert.AreEqual(...);
}
很多答案我都不信服。
首先,假设您要对使用 HttpClient
的方法进行单元测试。您不应该在您的实现中直接实例化 HttpClient
。您应该注入一个工厂,负责为您提供 HttpClient
的实例。这样你就可以稍后在那个工厂上模拟 return 你想要的 HttpClient
(例如:模拟 HttpClient
而不是真实的)。
因此,您将拥有一个如下所示的工厂:
public interface IHttpClientFactory
{
HttpClient Create();
}
和一个实现:
public class HttpClientFactory
: IHttpClientFactory
{
public HttpClient Create()
{
var httpClient = new HttpClient();
return httpClient;
}
}
当然你需要在你的 IoC 容器中注册这个实现。如果您使用 Autofac,它将类似于:
builder
.RegisterType<IHttpClientFactory>()
.As<HttpClientFactory>()
.SingleInstance();
现在您将拥有一个正确且可测试的实现。假设您的方法类似于:
public class MyHttpClient
: IMyHttpClient
{
private readonly IHttpClientFactory _httpClientFactory;
public SalesOrderHttpClient(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<string> PostAsync(Uri uri, string content)
{
using (var client = _httpClientFactory.Create())
{
var clientAddress = uri.GetLeftPart(UriPartial.Authority);
client.BaseAddress = new Uri(clientAddress);
var content = new StringContent(content, Encoding.UTF8, "application/json");
var uriAbsolutePath = uri.AbsolutePath;
var response = await client.PostAsync(uriAbsolutePath, content);
var responseJson = response.Content.ReadAsStringAsync().Result;
return responseJson;
}
}
}
现在是测试部分。 HttpClient
扩展了 HttpMessageHandler
,这是抽象的。让我们创建一个接受委托的 HttpMessageHandler
的“模拟”,这样当我们使用模拟时,我们还可以为每个测试设置每个行为。
public class MockHttpMessageHandler
: HttpMessageHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;
public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
{
_sendAsyncFunc = sendAsyncFunc;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _sendAsyncFunc.Invoke(request, cancellationToken);
}
}
现在,在 Moq(和 FluentAssertions,一个使单元测试更具可读性的库)的帮助下,我们拥有了对使用 HttpClient
public static class PostAsyncTests
{
public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
: Given_WhenAsync_Then_Test
{
private SalesOrderHttpClient _sut;
private Uri _uri;
private string _content;
private string _expectedResult;
private string _result;
protected override void Given()
{
_uri = new Uri("http://test.com/api/resources");
_content = "{\"foo\": \"bar\"}";
_expectedResult = "{\"result\": \"ok\"}";
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var messageHandlerMock =
new MockHttpMessageHandler((request, cancellation) =>
{
var responseMessage =
new HttpResponseMessage(HttpStatusCode.Created)
{
Content = new StringContent("{\"result\": \"ok\"}")
};
var result = Task.FromResult(responseMessage);
return result;
});
var httpClient = new HttpClient(messageHandlerMock);
httpClientFactoryMock
.Setup(x => x.Create())
.Returns(httpClient);
var httpClientFactory = httpClientFactoryMock.Object;
_sut = new SalesOrderHttpClient(httpClientFactory);
}
protected override async Task WhenAsync()
{
_result = await _sut.PostAsync(_uri, _content);
}
[Fact]
public void Then_It_Should_Return_A_Valid_JsonMessage()
{
_result.Should().BeEquivalentTo(_expectedResult);
}
}
}
显然这个测试很愚蠢,我们真的在测试我们的模拟。但是你明白了。您应该根据您的实现来测试有意义的逻辑,例如..
- 如果响应的代码状态不是201,是否应该抛出异常?
- 如果无法解析响应文本,应该怎么办?
- 等等
这个答案的目的是测试使用 HttpClient 的东西,这是一种非常干净的方法。
更新 最近我在我的测试中使用了一个 http 构建器,我可以在其中轻松地注入我期望的 json 响应。
public class HttpClientBuilder
{
private HttpMessageHandler _httpMessageHandler = new HttpClientHandler();
public HttpClientBuilder WithJsonResponse(HttpStatusCode httpStatusCode, string json, string contentType = "application/json")
{
var mockHttpMessageHandler =
new MockHttpMessageHandler(
(request, cancellation) =>
{
var responseMessage =
new HttpResponseMessage(httpStatusCode)
{
Content = new StringContent(json, Encoding.UTF8, contentType)
};
var result = Task.FromResult(responseMessage);
return result;
});
_httpMessageHandler = mockHttpMessageHandler;
return this;
}
public HttpClient Build()
{
var httpClient = new HttpClient(_httpMessageHandler);
return httpClient;
}
}
class MockHttpMessageHandler
: HttpMessageHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;
public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
{
_sendAsyncFunc = sendAsyncFunc;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _sendAsyncFunc.Invoke(request, cancellationToken);
}
}
所以,只要我有像 IHttpClientFactory
这样的抽象背后的 HttpClient,正如我上面所建议的,在我的测试中我可以做一些像
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var jsonResponse = "{\"hello world\"}";
var httpClient =
new HttpClientBuilder()
.WithJsonResponse(HttpStatusCode.OK, jsonResponse)
.Build();
httpClientFactoryMock
.Setup(x => x.Create())
.Returns(httpClient);
var httpClientFactory = httpClientFactoryMock.Object;
然后使用那个 httpClientFactory。
您所需要的只是 HttpMessageHandler
class 的测试版本,您将其传递给 HttpClient
ctor。要点是您的测试 HttpMessageHandler
class 将有一个 HttpRequestHandler
委托,调用者可以设置它并简单地按照他们想要的方式处理 HttpRequest
。
public class FakeHttpMessageHandler : HttpMessageHandler
{
public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
(r, c) =>
new HttpResponseMessage
{
ReasonPhrase = r.RequestUri.AbsoluteUri,
StatusCode = HttpStatusCode.OK
};
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(HttpRequestHandler(request, cancellationToken));
}
}
您可以使用此 class 的实例来创建具体的 HttpClient 实例。通过 HttpRequestHandler 委托,您可以完全控制来自 HttpClient 的传出 http 请求。
加入聚会有点晚,但我喜欢在具有下游 REST 依赖项的 dotnet 核心微服务的集成测试中尽可能使用 wiremocking (https://github.com/WireMock-Net/WireMock.Net)。
通过实现扩展 IHttpClientFactory 的 TestHttpClientFactory,我们可以覆盖方法
HttpClient CreateClient(字符串名称)
因此,当在您的应用程序中使用命名客户端时,您可以控制 return 连接到您的 Wiremock 的 HttpClient。
这种方法的好处是您不会在您正在测试的应用程序中更改任何内容,并启用课程集成测试对您的服务执行实际的 REST 请求并模拟 json(或其他)实际的下游请求应该 return。这会导致在您的应用程序中进行简洁的测试和尽可能少的模拟。
public class TestHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
var httpClient = new HttpClient
{
BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
// G.Config is our singleton config access, so the endpoint
// to the running wiremock is used in the test
};
return httpClient;
}
}
和
// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);
灵感来自
SystemUnderTest
是您要测试的 class - 没有给出示例内容,但我假设您已经有了!
[TestFixture]
public class HttpClientTests
{
private ISystemUnderTest _systemUnderTest;
private HttpMessageHandler _mockMessageHandler;
[SetUp]
public void Setup()
{
_mockMessageHandler = A.Fake<HttpMessageHandler>();
var httpClient = new HttpClient(_mockMessageHandler);
_systemUnderTest = new SystemUnderTest(httpClient);
}
[Test]
public void HttpError()
{
// Arrange
A.CallTo(_mockMessageHandler)
.Where(x => x.Method.Name == "SendAsync")
.WithReturnType<Task<HttpResponseMessage>>()
.Returns(Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.InternalServerError,
Content = new StringContent("abcd")
}));
// Act
var result = _systemUnderTest.DoSomething();
// Assert
// Assert.AreEqual(...);
}
}
这是一个简单的解决方案,对我来说效果很好。
使用最小起订量模拟库。
// ARRANGE
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock
.Protected()
// Setup the PROTECTED method to mock
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
// prepare the expected response of the mocked http call
.ReturnsAsync(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("[{'id':1,'value':'1'}]"),
})
.Verifiable();
// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
BaseAddress = new Uri("http://test.com/"),
};
var subjectUnderTest = new MyTestClass(httpClient);
// ACT
var result = await subjectUnderTest
.GetSomethingRemoteAsync('api/test/whatever');
// ASSERT
result.Should().NotBeNull(); // this is fluent assertions here...
result.Id.Should().Be(1);
// also check the 'http' call was like we expected it
var expectedUri = new Uri("http://test.com/api/test/whatever");
handlerMock.Protected().Verify(
"SendAsync",
Times.Exactly(1), // we expected a single external request
ItExpr.Is<HttpRequestMessage>(req =>
req.Method == HttpMethod.Get // we expected a GET request
&& req.RequestUri == expectedUri // to this uri
),
ItExpr.IsAny<CancellationToken>()
);
来源:https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/
也许您当前的项目中需要更改一些代码,但对于新项目,您绝对应该考虑使用 Flurl。
它是 .NET 的 HTTP 客户端库,具有流畅的界面,专门为使用它发出 HTTP 请求的代码启用可测试性。
网站上有很多代码示例,但简而言之,您可以在代码中像这样使用它。
添加用法。
using Flurl;
using Flurl.Http;
发送获取请求并读取响应。
public async Task SendGetRequest()
{
var response = await "https://example.com".GetAsync();
// ...
}
在单元测试中,Flurl 充当模拟,可以配置为按需要运行,还可以验证已完成的调用。
using (var httpTest = new HttpTest())
{
// Arrange
httpTest.RespondWith("OK", 200);
// Act
await sut.SendGetRequest();
// Assert
httpTest.ShouldHaveCalled("https://example.com")
.WithVerb(HttpMethod.Get);
}
由于HttpClient
使用SendAsync
方法执行所有HTTP Requests
,您可以override SendAsync
方法并模拟HttpClient
。
对于创建 HttpClient
到 interface
的包装,如下所示
public interface IServiceHelper
{
HttpClient GetClient();
}
然后使用上面 interface
在您的服务中进行依赖注入,示例如下
public class SampleService
{
private readonly IServiceHelper serviceHelper;
public SampleService(IServiceHelper serviceHelper)
{
this.serviceHelper = serviceHelper;
}
public async Task<HttpResponseMessage> Get(int dummyParam)
{
try
{
var dummyUrl = "http://www.dummyurl.com/api/controller/" + dummyParam;
var client = serviceHelper.GetClient();
HttpResponseMessage response = await client.GetAsync(dummyUrl);
return response;
}
catch (Exception)
{
// log.
throw;
}
}
}
现在在单元测试项目中创建一个助手 class 用于模拟 SendAsync
。
这是一个 FakeHttpResponseHandler
class 也就是 inheriting
DelegatingHandler
它将提供一个选项来覆盖 SendAsync
方法。覆盖 SendAsync
方法后,需要为调用 SendAsync
方法的每个 HTTP Request
设置一个响应,为此创建一个 Dictionary
,key
作为 Uri
和 value
作为 HttpResponseMessage
这样只要有 HTTP Request
并且如果 Uri
匹配 SendAsync
将 return 配置的 HttpResponseMessage
.
public class FakeHttpResponseHandler : DelegatingHandler
{
private readonly IDictionary<Uri, HttpResponseMessage> fakeServiceResponse;
private readonly JavaScriptSerializer javaScriptSerializer;
public FakeHttpResponseHandler()
{
fakeServiceResponse = new Dictionary<Uri, HttpResponseMessage>();
javaScriptSerializer = new JavaScriptSerializer();
}
/// <summary>
/// Used for adding fake httpResponseMessage for the httpClient operation.
/// </summary>
/// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
/// <param name="uri">Service end point URL.</param>
/// <param name="httpResponseMessage"> Response expected when the service called.</param>
public void AddFakeServiceResponse(Uri uri, HttpResponseMessage httpResponseMessage)
{
fakeServiceResponse.Remove(uri);
fakeServiceResponse.Add(uri, httpResponseMessage);
}
/// <summary>
/// Used for adding fake httpResponseMessage for the httpClient operation having query string parameter.
/// </summary>
/// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
/// <param name="uri">Service end point URL.</param>
/// <param name="httpResponseMessage"> Response expected when the service called.</param>
/// <param name="requestParameter">Query string parameter.</param>
public void AddFakeServiceResponse<TQueryStringParameter>(Uri uri, HttpResponseMessage httpResponseMessage, TQueryStringParameter requestParameter)
{
var serilizedQueryStringParameter = javaScriptSerializer.Serialize(requestParameter);
var actualUri = new Uri(string.Concat(uri, serilizedQueryStringParameter));
fakeServiceResponse.Remove(actualUri);
fakeServiceResponse.Add(actualUri, httpResponseMessage);
}
// all method in HttpClient call use SendAsync method internally so we are overriding that method here.
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if(fakeServiceResponse.ContainsKey(request.RequestUri))
{
return Task.FromResult(fakeServiceResponse[request.RequestUri]);
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
{
RequestMessage = request,
Content = new StringContent("Not matching fake found")
});
}
}
通过模拟框架或类似下面的方法为 IServiceHelper
创建一个新的实现。
这个 FakeServiceHelper
class 我们可以用来注入 FakeHttpResponseHandler
class 这样每当这个 class
创建的 HttpClient
它将使用 FakeHttpResponseHandler class
而不是实际的实现。
public class FakeServiceHelper : IServiceHelper
{
private readonly DelegatingHandler delegatingHandler;
public FakeServiceHelper(DelegatingHandler delegatingHandler)
{
this.delegatingHandler = delegatingHandler;
}
public HttpClient GetClient()
{
return new HttpClient(delegatingHandler);
}
}
并在测试中通过添加 Uri
和预期的 HttpResponseMessage
配置 FakeHttpResponseHandler class
。
Uri
应该是实际的 service
端点 Uri
以便当从实际 service
实现中调用 overridden SendAsync
方法时它将匹配 Uri
在 Dictionary
中并使用配置的 HttpResponseMessage
进行响应。
配置后将 FakeHttpResponseHandler object
注入到伪造的 IServiceHelper
实现中。
然后将 FakeServiceHelper class
注入实际服务,这将使实际服务使用 override SendAsync
方法。
[TestClass]
public class SampleServiceTest
{
private FakeHttpResponseHandler fakeHttpResponseHandler;
[TestInitialize]
public void Initialize()
{
fakeHttpResponseHandler = new FakeHttpResponseHandler();
}
[TestMethod]
public async Task GetMethodShouldReturnFakeResponse()
{
Uri uri = new Uri("http://www.dummyurl.com/api/controller/");
const int dummyParam = 123456;
const string expectdBody = "Expected Response";
var expectedHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(expectdBody)
};
fakeHttpResponseHandler.AddFakeServiceResponse(uri, expectedHttpResponseMessage, dummyParam);
var fakeServiceHelper = new FakeServiceHelper(fakeHttpResponseHandler);
var sut = new SampleService(fakeServiceHelper);
var response = await sut.Get(dummyParam);
var responseBody = await response.Content.ReadAsStringAsync();
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.AreEqual(expectdBody, responseBody);
}
}
经过仔细搜索,我找到了实现此目的的最佳方法。
private HttpResponseMessage response;
[SetUp]
public void Setup()
{
var handlerMock = new Mock<HttpMessageHandler>();
handlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
// This line will let you to change the response in each test method
.ReturnsAsync(() => response);
_httpClient = new HttpClient(handlerMock.Object);
yourClinet = new YourClient( _httpClient);
}
As you noticed I have used Moq and Moq.Protected packages.
添加我的 2 美分。模拟特定的 http 请求方法 Get 或 Post。这对我有用。
mockHttpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(a => a.Method == HttpMethod.Get), ItExpr.IsAny<CancellationToken>())
.Returns(Task.FromResult(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(""),
})).Verifiable();
Microsoft 现在建议使用 IHttpClientFactory
而不是直接使用 HttpClient
:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0
请求 returns 预期结果的模拟示例:
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;
}
来源:
不要 有一个创建 HttpClient 新实例的包装器。如果这样做,您将 运行 在 运行 时间用完套接字(即使您正在处理 HttpClient 对象)。
如果使用最小起订量,正确的做法是将 using Moq.Protected;
添加到您的测试中,然后编写如下代码:
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("It worked!")
};
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(() => response);
var httpClient = new HttpClient(mockHttpMessageHandler.Object);
如果你不介意运行你自己的http服务器,你可以试试Xim。就这么简单:
using Xim.Simulators.Api;
[Test]
public async Task TestHttpGetMethod()
{
using var simulation = Simulation.Create();
using var api = simulation
.AddApi()
.AddHandler("GET /books/1234", ApiResponse.Ok())
.Build();
await api.StartAsync();
var httpClient = new HttpClient();
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{api.Location}/books/1234"));
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
Assert.IsTrue(api.ReceivedApiCalls.Any(call => call.Action == "GET /books/1234"));
}
这是使用 mocks 的一个不错的替代方法,在某些情况下可能会满足您的需求。它建立在 Kestrel 之上(是的,我是作者)。