在单元测试中模拟 HttpClient

13 浏览
0 Comments

在单元测试中模拟 HttpClient

我在尝试将我的代码封装成单元测试时遇到了一些问题。问题是这样的。我有一个接口IHttpHandler

public interface IHttpHandler
{
    HttpClient client { get; }
}

还有使用它的类HttpHandler

public class HttpHandler : IHttpHandler
{
    public HttpClient client
    {
        get
        {
            return new HttpClient();
        }
    }
}

然后还有使用简单IOC来注入客户端实现的Connection类:

public class Connection
{
    private IHttpHandler _httpClient;
    public Connection(IHttpHandler httpClient)
    {
        _httpClient = httpClient;
    }
}

然后我有一个单元测试项目,其中有这个类:

private IHttpHandler _httpClient;
[TestMethod]
public void TestMockConnection()
{
    var client = new Connection(_httpClient);
    client.doSomething();  
    // 这里我想要创建一个模拟的http客户端实例
    // 而不是真正的实例。我应该如何解决这个问题?
}

现在显然,我将在Connection类中有从后端检索数据(JSON)的方法。然而,我想为这个类编写单元测试,显然我不想针对真正的后端编写测试,而是针对一个模拟的后端。我已经尝试通过谷歌来找到一个好的答案,但没有取得很大的成功。我以前可以并且已经使用Moq进行模拟,但从来没有在HttpClient这样的东西上。我应该如何解决这个问题?

0
0 Comments

问题的原因:接口暴露了具体的HttpClient类,因此使用该接口的类与它绑定在一起,这意味着它无法被模拟。

解决方法:编写自己的接口,并采用装饰者模式。代码如下:

public interface IHttpHandler
{
    HttpResponseMessage Get(string url);
    HttpResponseMessage Post(string url, HttpContent content);
    Task GetAsync(string url);
    Task PostAsync(string url, HttpContent content);
}
public class HttpClientHandler : IHttpHandler
{
    private HttpClient _client = new HttpClient();
    public HttpResponseMessage Get(string url)
    {
        return GetAsync(url).Result;
    }
    public HttpResponseMessage Post(string url, HttpContent content)
    {
        return PostAsync(url, content).Result;
    }
    public async Task GetAsync(string url)
    {
        return await _client.GetAsync(url);
    }
    public async Task PostAsync(string url, HttpContent content)
    {
        return await _client.PostAsync(url, content);
    }
}

这样做的重点是HttpClientHandler创建了自己的HttpClient,然后你当然可以创建多个实现IHttpHandler接口的类。

这种方法的主要问题是你实际上是在编写一个只调用另一个类中方法的类,但是你可以创建一个继承自HttpClient的类(参见Nkosi的示例,这是比我的方法更好的方法)。如果HttpClient有一个你可以模拟的接口,那就简单多了,但很遗憾它没有。

这个示例并不是完美的解决方案。IHttpHandler仍然依赖于HttpResponseMessage,它属于System.Net.Http命名空间,因此如果你需要使用除了HttpClient之外的其他实现,你将不得不执行某种映射,将它们的响应转换为HttpResponseMessage对象。当然,只有在需要使用多个IHttpHandler实现时才是个问题,但看起来你并不需要,所以这不是世界末日,但还是需要考虑一下。

无论如何,你可以简单地模拟IHttpHandler,而不必担心具体的HttpClient类,因为它已经被抽象掉了。

我建议测试非异步方法,因为这些方法仍然调用异步方法,但不需要担心单元测试异步方法的麻烦。

值得注意的是,这个答案和Nkosi的主要区别在于这是一个更薄的抽象。对于一个谦卑对象来说,薄可能是好的。

请注意,这应该被注册为一个Singleton。创建大量的HttpClient是不好的。

希望这些回答能对你有帮助,如果你觉得有用,请随时点赞。

0
0 Comments

【标题】在单元测试中模拟HttpClient

【引言】在进行单元测试时,如果涉及到对HttpClient的调用,我们可以使用模拟(mock)HttpMessageHandler来解决此问题。本文将介绍出现该问题的原因以及解决方法,并给出一个完整的示例。

【问题原因】HttpClient被设计为在应用程序的整个生命周期内实例化一次并进行重用。然而,在单元测试中,我们通常希望对HttpClient进行模拟,以避免实际进行网络请求,提高测试效率。

【解决方法】解决该问题的一种方法是模拟HttpMessageHandler,因为SendAsync方法是受保护的,我们可以使用Moq框架来进行模拟。下面是一个完整的示例,使用了xunit和Moq。

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;
namespace MockHttpClient {
    public class SiteAnalyzer {
        public SiteAnalyzer(HttpClient httpClient) {
            _httpClient = httpClient;
        }
        public async Task GetContentSize(string uri) {
            var response = await _httpClient.GetAsync(uri);
            var content = await response.Content.ReadAsStringAsync();
            return content.Length;
        }
        private readonly HttpClient _httpClient;
    }
    public class SiteAnalyzerTests {
        [Fact]
        public async void GetContentSizeReturnsCorrectLength() {
            // Arrange
            const string testContent = "test content";
            var mockMessageHandler = new Mock();
            mockMessageHandler.Protected()
                .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
                .ReturnsAsync(new HttpResponseMessage {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(testContent)
                });
            var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object));
            
            // Act
            var result = await underTest.GetContentSize("http://anyurl");
            
            // Assert
            Assert.Equal(testContent.Length, result);
        }
    }
}

该示例中,我们使用了Moq框架来模拟HttpMessageHandler。通过调用`mockMessageHandler.Protected().Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())`来模拟SendAsync方法的行为,并返回一个包含测试内容的HttpResponseMessage对象。然后,我们实例化了SiteAnalyzer类,并传入模拟的HttpClient对象。在测试方法中,我们调用GetContentSize方法,并断言返回的长度是否正确。

在进行单元测试时,如果涉及到对HttpClient的调用,我们可以使用Moq框架来模拟HttpMessageHandler,以避免实际进行网络请求。通过模拟SendAsync方法的行为,我们可以控制返回的HttpResponseMessage对象,从而进行测试。

0
0 Comments

问题的出现原因:

在单元测试中,当涉及到与外部资源进行交互的代码时,如使用 HttpClient 进行网络请求,就会遇到问题。由于 HttpClient 是一个具体的类,它与外部资源的交互是实时的,因此在测试时无法进行有效的控制和模拟。

解决方法:

为了解决这个问题,可以使用 MockHttpMessageHandler 来模拟 HttpClient 的行为。MockHttpMessageHandler 是 HttpClient 构造函数中传入的 HttpMessageHandler 的一个实现,它可以用于模拟网络请求并返回自定义的响应。

具体的解决方法如下所示:

1. 创建一个 MockHttpMessageHandler 的实例。

2. 使用 mockHttp.When 方法设置对特定 URL 的请求进行响应的规则。

3. 将 mockHttp 作为参数传递给 HttpClient 的构造函数,创建一个 HttpClient 的实例。

4. 使用 HttpClient 实例进行网络请求,并获取响应结果。

5. 在测试代码中,可以通过替换 HttpClient 的创建方式,来替换真实的网络请求。

示例代码如下所示:

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 = new HttpClient(mockHttp);
var response = await client.GetAsync("http://localhost/api/user/1234");
var json = await response.Content.ReadAsStringAsync();
Console.Write(json); // {'name' : 'Test McGee'}

对于私有静态的 HttpClient 实例的测试,可以通过在类中提供一种注入模拟 HttpClient 的方法,或者将模拟 HttpClient 作为构造函数的参数进行注入。

在单元测试中,当涉及到与外部资源进行交互的代码,如使用 HttpClient 进行网络请求时,可以使用 MockHttpMessageHandler 来模拟 HttpClient 的行为。通过设置模拟的规则,可以对特定的请求进行响应,并获取自定义的响应结果。这样就可以在单元测试中对与网络请求相关的代码进行有效的测试和验证。

0