模拟 HttpClient SendAsync 方法

mock HttpClient SendAsync method

我的代码使用 HttpClient 来检索一些数据

HttpClient client = new HttpClient
{
    BaseAddress = new Uri("myurl.com"),
};
var msg = new HttpRequestMessage(HttpMethod.Get, "myendpoint");
var res = await client.SendAsync(msg);

如何在 HttpClient 上模拟此 SendAsync 方法并将其注入到 .net 核心 ServiceCollection 中?

我试过这样模拟

var mockFactory = new Mock<IHttpClientFactory>();
            var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
            mockHttpMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent("{'name':thecodebuzz,'city':'USA'}"),
                });

            var client = new HttpClient(mockHttpMessageHandler.Object);
            mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);

but how to inject this mockFactory into ServiceCollection? Or maybe there is some easier or different way around?

如果你真的需要模拟 HttpClient 本身,看看这个库: https://github.com/richardszalay/mockhttp

来自文档:

var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localhost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();

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'}

与其模拟 HTTP 调用,不如封装它?然后你可以模拟 encapsulation/abstraction.

例如:

interface IClient
{
  Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default);
}

class HttpClientAdapter : IClient
{
  readonly HttpClient _client;

  public HttpClientAdapter(HttpClient client)
  {
    _client = client;
  }

  public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) => _client.SendAsync(request, cancellationToken);
}

使您的代码依赖于 IClient 接口。在正常使用期间,您将在 HttpClientAdapter 实现中使用真正的 HttpClient。对于测试,您可以模拟 IClient.

请注意,使抽象级别比这高一点可能对您更有用。例如,如果您希望将来自 HTTP 响应的 JSON 字符串解析为一些 DataObject,那么您的 IClient 界面可能看起来更像这样:

class DataObject
{
  public string Name { get; set; }
  public string City { get; set; }
}

interface IClient
{
  Task<DataObject> GetAsync(CancellationToken cancellationToken = default);
}

public class ClientImplementation : IClient
{
  readonly HttpClient _client;

  public ClientImplementation(HttpClient client)
  {
    _client = client;
  }

  public async Task<DataObject> GetAsync(CancellationToken cancellationToken = default)
  {
    var response = await _client.SendAsync(...);
    var dataObject = new DataObject();
    // parse the response into the data object
    return dataObject;
  }
}

在这里画线的好处是您的测试工作量会减少。例如,您的模拟代码不必设置 HttpResponseMessage 个对象。

您选择在哪里为您的抽象划定界限完全取决于您。但关键要点是:一旦您的代码依赖于一个小接口,那么模拟该接口并测试您的代码就很容易了。