首页 > 解决方案 > 重构代码以允许对 HttpClient 进行单元测试

问题描述

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

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. 在阅读了此处的其他答案和 两篇文章之后,我知道解决此问题的更好方法之一是模拟它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<HttpClient>();到 的ConfigureServices方法Startup.cs

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

标签: c#.netunit-testing.net-coredotnet-httpclient

解决方案


我发现最简单的方法是继续使用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可能是另一个不错的选择,但我没有玩太多,所以我只会提供我自己发现有用的信息。


推荐阅读