为了账号安全,请及时绑定邮箱和手机立即绑定

在单元测试中模拟HttpClient

/ 猿问

在单元测试中模拟HttpClient

白衣染霜花 2019-12-21 12:47:44

我在尝试包装要在单元测试中使用的代码时遇到了一些问题。问题是这样的。我有接口IHttpHandler:


public interface IHttpHandler

{

    HttpClient client { get; }

}

和使用它的类,HttpHandler:


public class HttpHandler : IHttpHandler

{

    public HttpClient client

    {

        get

        {

            return new HttpClient();

        }

    }

}

然后是Connection类,该类使用simpleIOC注入客户端实现:


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();  


    // Here I want to somehow create a mock instance of the http client

    // Instead of the real one. How Should I approach this?     


}

现在显然我将在Connection类中具有从后端检索数据(JSON)的方法。但是,我想为此类编写单元测试,显然我不想针对真正的后端编写测试,而只是想模拟一个后端。我曾试图用谷歌搜索一个很好的答案,但没有成功。我可以并且曾经使用过Moq进行模拟,但是从未使用过诸如httpClient之类的东西。我应该如何解决这个问题?


提前致谢。


查看完整描述

3 回答

?
天天世纪

您的接口公开了具体的HttpClient类,因此,使用该接口的所有类都将与其绑定在一起,这意味着无法对其进行模拟。


HttpClient不会从任何接口继承,因此您必须编写自己的接口。我建议一种类似装饰器的模式:


public interface IHttpHandler

{

    HttpResponseMessage Get(string url);

    HttpResponseMessage Post(string url, HttpContent content);

    Task<HttpResponseMessage> GetAsync(string url);

    Task<HttpResponseMessage> 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<HttpResponseMessage> GetAsync(string url)

    {

        return await _client.GetAsync(url);

    }


    public async Task<HttpResponseMessage> 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类被抽象化了。


查看完整回答
反对 回复 2019-12-21
?
MMMHUHU

HttpClient的可扩展性在于HttpMessageHandler传递给构造函数。其目的是允许特定于平台的实现,但是您也可以对其进行模拟。无需为HttpClient创建装饰器包装器。


如果您更喜欢使用DSL而不是Moq,那么我在GitHub / Nuget上有一个库,可以使事情变得简单一些:https : //github.com/richardszalay/mockhttp


var mockHttp = new MockHttpMessageHandler();


// Setup a respond for the user api (including a wildcard in the URL)

mockHttp.When("http://localost/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");

// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;


var json = await response.Content.ReadAsStringAsync();


// No network connection required

Console.Write(json); // {'name' : 'Test McGee'}


查看完整回答
反对 回复 2019-12-21
?
慕尼黑的夜晚无繁华

我同意其他一些答案,即最佳方法是模拟HttpMessageHandler而不是包装HttpClient。这个答案是唯一的,因为它仍然会注入HttpClient,从而使其成为单例或通过依赖项注入进行管理。


“ HttpClient旨在实例化一次,并在应用程序的整个生命周期内重复使用。” (来源)。


因为SendAsync受保护,所以模拟HttpMessageHandler可能会有些棘手。这是使用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;

// Use nuget to install xunit and Moq


namespace MockHttpClient {

    class Program {

        static void Main(string[] args) {

            var analyzer = new SiteAnalyzer(Client);

            var size = analyzer.GetContentSize("http://microsoft.com").Result;

            Console.WriteLine($"Size: {size}");

        }


        private static readonly HttpClient Client = new HttpClient(); // Singleton

    }


    public class SiteAnalyzer {

        public SiteAnalyzer(HttpClient httpClient) {

            _httpClient = httpClient;

        }


        public async Task<int> 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<HttpMessageHandler>();

            mockMessageHandler.Protected()

                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())

                .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);

        }

    }

}


查看完整回答
反对 回复 2019-12-21

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信