C# 重构代码以允许对HttpClient进行单元测试

C# 重构代码以允许对HttpClient进行单元测试,c#,.net,unit-testing,.net-core,dotnet-httpclient,C#,.net,Unit Testing,.net Core,Dotnet Httpclient,我正在处理的代码如下所示: public class Uploader : IUploader { public Uploader() { // assign member variables to dependency injected interface implementations } public async Task<string> Upload(string url, string data) {

我正在处理的代码如下所示:

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
        }
    }
}
并添加:
services.AddSingleton()
ConfigureServices
方法的
Startup.cs


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

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

IHttpService

public interface IHttpService
{
    Task<HttpResponseMessage> Post(Uri url, string payload, Dictionary<string, string> headers = null);
}
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);
    }
}
然后,您可以在单元测试中使用mock
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);      
    }
}
[TestClass]
公共类上传测试
{
私有Mock_mockHttpService=new Mock();
[测试方法]
状态代码不为200OK时的公共异步任务\u ThenErrorMessageReturned()
{
//安排
var uploader=新的上传程序(_mockHttpService.Object);
var url=“someurl.co.uk”;
var data=“data”;
//需要设置模拟以返回要测试的响应
_mockHttpService
.Setup(s=>s.PostAsync(url、数据))
.ReturnsAsync(新的HttpResponseMessage(HttpStatusCode.InternalServerError));
//表演
var result=wait uploader.Upload(新的Uri(url),数据);
//断言
AreEqual(“某些错误消息”,结果);
}
[测试方法]
状态代码为200OK时的公共异步任务\u然后返回Null()
{
//安排
var uploader=新的上传程序(_mockHttpService.Object);
var url=“someurl.co.uk”;
var data=“data”;
//需要设置模拟以返回要测试的响应
_mockHttpService
.Setup(s=>s.PostAsync(新Uri(url),数据))
.ReturnsAsync(新的HttpResponseMessage(HttpStatusCode.OK));
//表演
var result=wait uploader.Upload(url,数据);
//断言
aresequal(null,result);
}
}

我发现最简单的方法是继续使用
HttpClient
,但传入一个mock
HttpClientHandler
,例如

上面链接中的代码示例:

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内核中内置的依赖项注入框架忽略了
内部的
构造函数,因此在此场景中它将调用无参数构造函数

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
可能是另一个不错的选择,但我还没有过多地尝试过,所以我只提供一些我发现有用的信息。

如果使用asp.net core,请阅读HttpClientFactoryHttpClient将所有调用委托给HttpClientHandler。要模拟HttpClient,请使用模拟处理程序。您可以通过使用HttpClientFactory并在测试中指定模拟处理程序来实现自动化
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'}
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
}
[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);
}