【问题标题】:Refactoring code to allow for unit testing of HttpClient重构代码以允许对 HttpClient 进行单元测试
【发布时间】:2019-10-26 01:42:08
【问题描述】:

我正在处理一段看起来像这样的代码:

public class Uploader : IUploader
{
    public Uploader()
    {
        // assign member variables to dependency injected interface implementations
    }

    public async Task<string> Upload(string url, string data)
    {
        HttpResponseMessage result;
        try
        {
            var handler = new HttpClientHandler();
            var client = new HttpClient(handler);

            result = await client.PostAsync(url, new FormUrlEncodedContent(data));

            if (result.StatusCode != HttpStatusCode.OK)
            {
                return "Some Error Message";
            }
            else
            {
                return null; // Success!
            }
        }
        catch (Exception ex)
        {
            // do some fancy stuff here
        }
    }
}

我正在尝试对Upload 函数进行单元测试。特别是,我需要模拟HttpClient。在阅读了此处的其他答案和 these two 文章后,我知道解决此问题的更好方法之一是模拟 HttpMessageHandler 并将其传递给 HttpClient 并让它返回我想要的任何内容。

所以,我首先在构造函数中传入HttpClient 作为依赖项:

public class Uploader : IUploader
{
    private readonly HttpClient m_httpClient; // made this a member variable

    public Uploader(HttpClient httpClient) // dependency inject this
    {
        m_httpClient = httpClient;
    }

    public async Task<string> Upload(string url, string data)
    {
        HttpResponseMessage result;
        try
        {
            var handler = new HttpClientHandler();

            result = await m_httpClient.PostAsync(url, new FormUrlEncodedContent(data));

            if (result.StatusCode != HttpStatusCode.OK)
            {
                return "Some Error Message";
            }
            else
            {
                return null; // Success!
            }
        }
        catch (Exception ex)
        {
            // do some fancy stuff here
        }
    }
}

并将:services.AddSingleton&lt;HttpClient&gt;(); 添加到 Startup.csConfigureServices 方法中。

但是现在我面临一个小问题,原始代码专门创建了一个 HttpClientHandler 来传递。那么我该如何重构它以接受一个可模拟的处理程序?

【问题讨论】:

  • 如果使用 asp.net core,请阅读 HttpClientFactory
  • HttpClient 将所有调用委托给 HttpClientHandler。要模拟 HttpClient,请使用模拟处理程序。您可以通过使用 HttpClientFactory 并在测试中指定模拟处理程序来自动执行此操作

标签: c# .net unit-testing .net-core dotnet-httpclient


【解决方案1】:

一种方法是将您的 HTTP 功能抽象为一个服务,即实现IHttpService 接口的HttpService

IHttpService

public interface IHttpService
{
    Task<HttpResponseMessage> Post(Uri url, string payload, Dictionary<string, string> headers = null);
}

HttpService

public class HttpService : IHttpService
{
    private static HttpClient _httpClient;

    private const string MimeTypeApplicationJson = "application/json";

    public HttpService()
    {
        _httpClient = new HttpClient();
    }

    private static async Task<HttpResponseMessage> HttpSendAsync(HttpMethod method, Uri url, string payload,
        Dictionary<string, string> headers = null)
    {
        var request = new HttpRequestMessage(method, url);
        request.Headers.Add("Accept", MimeTypeApplicationJson);

        if (headers != null)
        {
            foreach (var header in headers)
            {
                request.Headers.Add(header.Key, header.Value);
            }
        }

        if (!string.IsNullOrWhiteSpace(payload))
            request.Content = new StringContent(payload, Encoding.UTF8, MimeTypeApplicationJson);

        return await _httpClient.SendAsync(request);
    }

    public async Task<HttpResponseMessage> Post(Uri url, string payload, Dictionary<string, string> headers = null)
    {
        return await HttpSendAsync(HttpMethod.Post, url, payload, headers);
    }
}

添加到您的服务中:

services.AddSingleton<IHttpService, HttpService>();

然后,您将在您的班级中注入 IHttpService 作为依赖项:

public class Uploader : IUploader
{
    private readonly IHttpService _httpService; // made this a member variable

    public Uploader(IHttpService httpService) // dependency inject this
    {
        _httpService = httpService;
    }

    public async Task<string> Upload(string url, string data)
    {
        HttpResponseMessage result;
        try
        {

            result = await _httpService.PostAsync(new Uri(url), data);

            if (result.StatusCode != HttpStatusCode.OK)
            {
                return "Some Error Message";
            }
            else
            {
                return null; // Success!
            }
        }
        catch (Exception ex)
        {
            // do some fancy stuff here
        }
    }
}

然后您可以在单元测试中使用Moq 模拟HttpService

[TestClass]
public class UploaderTests
{
    private Mock<IHttpService> _mockHttpService = new Mock<IHttpService>();

    [TestMethod]
    public async Task WhenStatusCodeIsNot200Ok_ThenErrorMessageReturned()
    {
        // arrange  
        var uploader = new Uploader(_mockHttpService.Object);
        var url = "someurl.co.uk";
        var data = "data";

        // need to setup your mock to return the response you want to test
        _mockHttpService
            .Setup(s => s.PostAsync(url, data))
            .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError));

        // act
        var result = await uploader.Upload(new Uri(url), data);

        // assert
        Assert.AreEqual("Some Error Message", result);      
    }

    [TestMethod]
    public async Task WhenStatusCodeIs200Ok_ThenNullReturned()
    {
        // arrange  
        var uploader = new Uploader(_mockHttpService.Object);
        var url = "someurl.co.uk";
        var data = "data";

        // need to setup your mock to return the response you want to test
        _mockHttpService
            .Setup(s => s.PostAsync(new Uri(url), data))
            .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

        // act
        var result = await uploader.Upload(url, data);

        // assert
        Assert.AreEqual(null, result);      
    }
}

【讨论】:

    【解决方案2】:

    我发现最简单的方法是继续使用HttpClient,但传入一个模拟HttpClientHandler,例如https://github.com/richardszalay/mockhttp

    以上链接中的代码示例:

    var mockHttp = new MockHttpMessageHandler();
    
    mockHttp.When("http://localhost/api/user/*")
            .Respond("application/json", "{'name' : 'Test McGee'}");
    
    // Inject the handler or client into your application code
    var client = mockHttp.ToHttpClient();
    
    var response = await client.GetAsync("http://localhost/api/user/1234");
    
    var json = await response.Content.ReadAsStringAsync();
    
    Console.Write(json); // {'name' : 'Test McGee'}
    

    .NET Core 中内置的依赖注入框架会忽略internal 构造函数,因此在这种情况下会调用无参数构造函数。

    public sealed class Uploader : IUploader
    {
        private readonly HttpClient m_httpClient;
    
        public Uploader() : this(new HttpClientHandler())
        {
        }
    
        internal Uploader(HttpClientHandler handler)
        {
            m_httpClient = new HttpClient(handler);
        }
    
        // regular methods
    }
    

    在你的单元测试中,你可以使用接受HttpClientHandler的构造函数:

    [Fact]
    public async Task ShouldDoSomethingAsync()
    {
        var mockHttp = new MockHttpMessageHandler();
    
        mockHttp.When("http://myserver.com/upload")
            .Respond("application/json", "{'status' : 'Success'}");
    
        var uploader = new Uploader(mockHttp);
    
        var result = await uploader.UploadAsync();
    
        Assert.Equal("Success", result.Status);
    }
    

    通常我不太喜欢使用内部构造函数来促进测试,但是,我发现这比注册共享 HttpClient 更明显和独立。

    HttpClientFactory 可能是另一个不错的选择,但我没有玩太多,所以我只会提供我自己发现有用的信息。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-07-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-03
      • 1970-01-01
      • 1970-01-01
      • 2016-02-02
      • 1970-01-01
      相关资源
      最近更新 更多