单元测试中的HttpClient使用

2021-11-12  乐帮网

c# unittest

已经有很多关于类如何以及是否可以测试的讨论HttpClient。我想写一篇简短的文章,为您提供三个选项,当您需要编写涉及HttpClient。假设我们有一个从 API 获取歌曲列表的简单类。我将使用它作为我们希望测试的示例类

public class SongService 
{
    private readonly HttpClient _httpClient;

    public SongService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<List<Song>> GetSongs() 
    {
        var requestUri = "http://api.songs.com/songs";
        var response = await _httpClient.GetAsync(requestUri);

        var responseData = response.Content.ReadAsString();
        var songs = JsonConvert.DeserializeObject<List<Song>>(responseData);

        return songs;
    }
}

1. 包装HttpClient

包装 HttpClient 将使我们能够拥有一个接口。反过来,我们可以更新Songs类以期望该接口。
这会给我们带来两个好处。首先,SongService它将得到改进,因为它将依赖于接口而不是具体的类。其次,我们可以模拟该接口以使其SongService更易于测试。

IHttpProvider.cs

public interface IHttpProvider
{
    Task<HttpResponseMessage> GetAsync(string requestUri);
    Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content);
    Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content);
    Task<HttpResponseMessage> DeleteAsync(string requestUri);
}

HttpProvider.cs

public class HttpClientProvider : IHttpClientProvider
    {
        private readonly HttpClient _httpClient;

        public HttpClientProvider(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public Task<HttpResponseMessage> GetAsync(string requestUri)
        {
            return _httpClient.GetAsync(requestUri);
        }

        public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content)
        {
            return _httpClient.PostAsync(requestUri, content);
        }

        public Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content)
        {
            return _httpClient.PutAsync(requestUri, content);
        }

        public Task<HttpResponseMessage> DeleteAsync(string requestUri)
        {
            return _httpClient.DeleteAsync(requestUri);
        }
    }

我已经包装了HttpClient. 但是您可能希望在您的实现中或多或少地公开。例如,如果您只需要该GetAsync方法,则只需执行以下操作。

IHttpProvider.cs

public interface IHttpProvider
{
    Task<HttpResponseMessage> GetAsync(string requestUri);
}

HttpProvider.cs

public class HttpClientProvider : IHttpClientProvider
    {
        private readonly HttpClient _httpClient;

        public HttpClientProvider(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public Task<HttpResponseMessage> GetAsync(string requestUri)
        {
            return _httpClient.GetAsync(requestUri);
        }
    }

现在我们可以SongService像这样更改以接受我们的新界面。

Song.cs

public class SongService 
{
    private readonly IHttpProvider _httpProvider;

    public SongService(IHttpProvider httpProvider)
    {
        _httpProvider = httpProvider;
    }

    public async Task<List<Song>> GetSongsAsync() 
    {
        var requestUri = "http://api.songs.com/songs";
        var response = await _httpProvider.GetAsync(requestUri);

        var responseData = response.Content.ReadAsString();
        var songs = JsonConvert.DeserializeObject<List<Song>>(responseData);

        return songs;
    }
}

我们现在可以SongService很容易地测试,因为我们有一个可以模拟的界面。

2. 模拟HttpMessageHandler

它HttpClient本身实际上是一个抽象层。它HttpMessageHandler决定了如何发送请求。
从测试的角度来看,这是个好消息。是不是HttpClient有一个构造函数,它接受一个 type 的参数HttpMessageHandler。这意味着我们可以根据需要创建一个虚假的处理程序设置并将其传递给测试。
FakeHttpMessageHandler.cs

public class FakeHttpMessageHandler : HttpMessageHandler
{
    public virtual HttpResponseMessage Send(HttpRequestMessage request)
    {
        // Configure this method however you wish for your testing needs.
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(Send(request));
    }
}

AwesomeUnitTests.cs

[Test]
public void AwesomeUnitTest1()
{
    // Arrange
    var fakeHttpMessageHandler = new Mock<FakeHttpMessageHandler> { CallBase = true };

    fakeHttpMessageHandler.Setup(x => x.Send(It.IsAny<HttpRequestMessage>()))
                          .Returns(new HttpResponseMessage(HttpStatusCode.OK));

    var fakeHttpClient = new HttpClient(fakeHttpMessageHandler.Object);

    // Act
    ...

    // Assert
    ...
}

使用这种方法,我们可以测试SongService而不需要更改任何代码。然而,我们被困在我们的FakeHttpMessageHandler类里。

3. 模拟HttpMessageHandler

我见过这种方法使用过几次,但只使用了Moq框架。
Moq 有一个扩展方法Protected(),您可以通过导入Moq.Protected命名空间来访问它。什么是Protected扩展方法所做的是让你嘲笑非公开protected成员。
这对我们很有用,因为HttpMessageHandler的SendAsync方法范围是protected internal。这意味着我们可以执行以下操作。

AwesomeUnitTests.cs

[Test]
public async Task AwesomeUnitTest2()
{
    // Arrange
    var mockHandler = new Mock<HttpMessageHandler>();

    mockHandler.Protected()
               .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>())
               .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
    // Act
    ...
    
    // Assert
    ...
}

再一次,我们现在SongService无需更改任何代码即可测试。但是这次我们没有留下额外的测试类。
这种方法的一个小缺点是我们必须使用一个可变的字符串来声明我们想要模拟的方法。但这可以通过使用字符串常量来轻松改进。

结论

因此,您可以通过三种不同的方式测试使用HttpClient. 希望至少其中之一对您有用。
处理HttpClient单元测试的首选方法是什么?此外,如果您有任何问题或意见,请在下方留言

原文:https://chrissainty.com/unit-testing-with-httpclient/

公众号二维码

关注我的微信公众号
在公众号里留言交流
投稿邮箱:1052839972@qq.com

庭院深深深几许?杨柳堆烟,帘幕无重数。
玉勒雕鞍游冶处,楼高不见章台路。
雨横风狂三月暮。门掩黄昏,无计留春住。
泪眼问花花不语,乱红飞过秋千去。

欧阳修

付款二维码

如果感觉对您有帮助
欢迎向作者提供捐赠
这将是创作的最大动力