我有一些问题,试图包装我的代码在单元测试中使用。问题在于。我有接口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这样的东西上使用过。我应该如何处理这个问题?


当前回答

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

如果你更喜欢DSL而不是使用Moq,我在GitHub/Nuget上有一个库,它可以让事情变得更容易一些:https://github.com/richardszalay/mockhttp

Nuget包RichardSzalay。MockHttp可以在这里找到。

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");
// 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'}

其他回答

基于其他答案,我建议这样的代码,它没有任何外部依赖:

[TestClass]
public class MyTestClass
{
    [TestMethod]
    public async Task MyTestMethod()
    {
        var httpClient = new HttpClient(new MockHttpMessageHandler());

        var content = await httpClient.GetStringAsync("http://some.fake.url");

        Assert.AreEqual("Content as string", content);
    }
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Content as string")
        };

        return await Task.FromResult(responseMessage);
    }
}

受PointZeroTwo答案的启发,下面是一个使用NUnit和FakeItEasy的示例。

本例中的SystemUnderTest是您想要测试的类-没有为它提供示例内容,但我假设您已经有了它!

[TestFixture]
public class HttpClientTests
{
    private ISystemUnderTest _systemUnderTest;
    private HttpMessageHandler _mockMessageHandler;

    [SetUp]
    public void Setup()
    {
        _mockMessageHandler = A.Fake<HttpMessageHandler>();
        var httpClient = new HttpClient(_mockMessageHandler);

        _systemUnderTest = new SystemUnderTest(httpClient);
    }

    [Test]
    public void HttpError()
    {
        // Arrange
        A.CallTo(_mockMessageHandler)
            .Where(x => x.Method.Name == "SendAsync")
            .WithReturnType<Task<HttpResponseMessage>>()
            .Returns(Task.FromResult(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.InternalServerError,
                Content = new StringContent("abcd")
            }));

        // Act
        var result = _systemUnderTest.DoSomething();

        // Assert
        // Assert.AreEqual(...);
    }
}

我的一个同事注意到,大多数HttpClient方法都在底层调用SendAsync(HttpRequestMessage request, CancellationToken CancellationToken),这是HttpMessageInvoker的一个虚拟方法:

所以到目前为止,模拟HttpClient最简单的方法就是简单地模拟这个特定的方法:

var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);

并且您的代码可以调用大多数(但不是全部)HttpClient类方法,包括常规方法

httpClient.SendAsync(req)

点击这里确认 https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs

我做了一些非常简单的事情,因为我在一个依赖注入环境中。

public class HttpHelper : IHttpHelper
{
    private ILogHelper _logHelper;

    public HttpHelper(ILogHelper logHelper)
    {
        _logHelper = logHelper;
    }

    public virtual async Task<HttpResponseMessage> GetAsync(string uri, Dictionary<string, string> headers = null)
    {
        HttpResponseMessage response;
        using (var client = new HttpClient())
        {
            if (headers != null)
            {
                foreach (var h in headers)
                {
                    client.DefaultRequestHeaders.Add(h.Key, h.Value);
                }
            }
            response = await client.GetAsync(uri);
        }

        return response;
    }

    public async Task<T> GetAsync<T>(string uri, Dictionary<string, string> headers = null)
    {
        ...

        rawResponse = await GetAsync(uri, headers);

        ...
    }

}

这个笑话是:

    [TestInitialize]
    public void Initialize()
    {
       ...
        _httpHelper = new Mock<HttpHelper>(_logHelper.Object) { CallBase = true };
       ...
    }

    [TestMethod]
    public async Task SuccessStatusCode_WithAuthHeader()
    {
        ...

        _httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns(
            Task<HttpResponseMessage>.Factory.StartNew(() =>
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(_testData))
                };
            })
        );
        var result = await _httpHelper.Object.GetAsync<TestDTO>(...);

        Assert.AreEqual(...);
    }

这是一个常见的问题,我非常希望能够模拟HttpClient,但我想我最终意识到不应该模拟HttpClient。这样做似乎是合乎逻辑的,但我认为我们已经被我们在开源库中看到的东西洗脑了。

We often see "Clients" out there that we mock in our code so that we can test in isolation, so we automatically try to apply the same principle to HttpClient. HttpClient actually does a lot; you can think of it as a manager for HttpMessageHandler, so you don't wanna mock that, and that's why it still doesn't have an interface. The part that you're really interested in for unit testing, or designing your services, even, is the HttpMessageHandler since that is what returns the response, and you can mock that.

同样值得指出的是,您可能应该开始把HttpClient当作一个更大的交易来对待。例如:让你的新HttpClients的实例化最小化。重复使用它们,它们被设计成可重复使用的,如果你这样做,会使用更少的资源。如果您开始把它当作一个更大的事情来对待,那么想要模拟它就会感觉更错误,现在消息处理程序将开始成为您正在注入的东西,而不是客户端。

换句话说,围绕处理程序而不是客户端设计依赖项。更好的是,使用HttpClient的抽象“服务”允许你注入一个处理程序,并将其作为你的可注入依赖项。事实上,HttpClientFactor(您应该使用它)被设计为带有注入消息处理程序的扩展。然后在测试中,可以伪造处理程序来控制设置测试的响应。

包装HttpClient是一种疯狂的时间浪费。

更新: 请看约书亚·杜姆斯的例子。这正是我所推荐的。