我有一些问题,试图包装我的代码在单元测试中使用。问题在于。我有接口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,但我想我最终意识到不应该模拟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是一种疯狂的时间浪费。

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

其他回答

经过仔细搜索,我找到了实现这一目标的最佳方法。

    private HttpResponseMessage response;

    [SetUp]
    public void Setup()
    {
        var handlerMock = new Mock<HttpMessageHandler>();

        handlerMock
           .Protected()
           .Setup<Task<HttpResponseMessage>>(
              "SendAsync",
              ItExpr.IsAny<HttpRequestMessage>(),
              ItExpr.IsAny<CancellationToken>())
           // This line will let you to change the response in each test method
           .ReturnsAsync(() => response);

        _httpClient = new HttpClient(handlerMock.Object);

        yourClinet = new YourClient( _httpClient);
    }

正如你所注意到的,我使用了Moq和Moq。保护包。

正如注释中提到的,您需要抽象HttpClient,这样就不会与它耦合。我以前也做过类似的事情。我会试着把我的方法和你们要做的方法相适应。

首先看一下HttpClient类,并决定需要它提供哪些功能。

以下是一种可能性:

public interface IHttpClient {
    System.Threading.Tasks.Task<T> DeleteAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> DeleteAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> PostAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PostAsync<T>(Uri uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(Uri uri, object package);
}

如前所述,这是为了特定目的。我完全抽象出了对HttpClient的依赖,并专注于我想要返回的东西。您应该评估如何抽象HttpClient以只提供您想要的必要功能。

这将允许您只模拟需要测试的内容。

我甚至建议完全放弃IHttpHandler,而使用HttpClient抽象IHttpClient。但我只是没有选择,因为你可以用抽象客户端的成员替换你的处理程序接口的主体。

IHttpClient的实现可以用来包装/改编一个真正的/具体的HttpClient或任何其他对象,它可以用来发出HTTP请求,因为你真正想要的是一个服务,它提供了与HttpClient相反的功能。使用抽象是一种干净(我的观点)和可靠的方法,并且可以使您的代码更易于维护,如果您需要在框架更改时将底层客户端切换为其他东西。

下面是如何实现的代码片段。

/// <summary>
/// HTTP Client adaptor wraps a <see cref="System.Net.Http.HttpClient"/> 
/// that contains a reference to <see cref="ConfigurableMessageHandler"/>
/// </summary>
public sealed class HttpClientAdaptor : IHttpClient {
    HttpClient httpClient;

    public HttpClientAdaptor(IHttpClientFactory httpClientFactory) {
        httpClient = httpClientFactory.CreateHttpClient(**Custom configurations**);
    }

    //...other code

     /// <summary>
    ///  Send a GET request to the specified Uri as an asynchronous operation.
    /// </summary>
    /// <typeparam name="T">Response type</typeparam>
    /// <param name="uri">The Uri the request is sent to</param>
    /// <returns></returns>
    public async System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class {
        var result = default(T);
        //Try to get content as T
        try {
            //send request and get the response
            var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
            //if there is content in response to deserialize
            if (response.Content.Headers.ContentLength.GetValueOrDefault() > 0) {
                //get the content
                string responseBodyAsText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                //desrialize it
                result = deserializeJsonToObject<T>(responseBodyAsText);
            }
        } catch (Exception ex) {
            Log.Error(ex);
        }
        return result;
    }

    //...other code
}

正如你在上面的例子中所看到的,很多通常与使用HttpClient相关的繁重工作都隐藏在抽象后面。

然后可以用抽象客户端注入连接类

public class Connection
{
    private IHttpClient _httpClient;

    public Connection(IHttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

然后,您的测试可以模拟SUT所需的内容

private IHttpClient _httpClient;

[TestMethod]
public void TestMockConnection()
{
    SomeModelObject model = new SomeModelObject();
    var httpClientMock = new Mock<IHttpClient>();
    httpClientMock.Setup(c => c.GetAsync<SomeModelObject>(It.IsAny<string>()))
        .Returns(() => Task.FromResult(model));

    _httpClient = httpClientMock.Object;

    var client = new Connection(_httpClient);

    // Assuming doSomething uses the client to make
    // a request for a model of type SomeModelObject
    client.doSomething();  
}

我同意其他一些回答,最好的方法是在HttpClient内部模拟HttpMessageHandler,而不是包装HttpClient。这个答案是唯一的,因为它仍然注入HttpClient,允许它成为一个单例或者使用依赖注入进行管理。

HttpClient打算被实例化一次,并在整个过程中被重用 应用程序的生命周期。

(来源)。

模拟HttpMessageHandler可能有点棘手,因为SendAsync是受保护的。下面是一个使用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);
        }
    }
}

也许在您当前的项目中会有一些代码需要更改,但对于新项目,您绝对应该考虑使用Flurl。

https://flurl.dev

它是一个。net的HTTP客户端库,具有一个流畅的接口,特别支持使用它来发出HTTP请求的代码的可测试性。

网站上有很多代码示例,但简单地说,你在代码中是这样使用的。

添加用途。

using Flurl;
using Flurl.Http;

发送get请求并读取响应。

public async Task SendGetRequest()
{
   var response = await "https://example.com".GetAsync();
   // ...
}

在单元测试中,Flurl充当一个模拟,可以将其配置为所需的行为,还可以验证所执行的调用。

using (var httpTest = new HttpTest())
{
   // Arrange
   httpTest.RespondWith("OK", 200);

   // Act
   await sut.SendGetRequest();

   // Assert
   httpTest.ShouldHaveCalled("https://example.com")
      .WithVerb(HttpMethod.Get);
}

有几种不同的方法来模拟HttpClient。以下是我在决定使用单一解决方案(Moq.Contrib.HttpClient)之前对xUnit做的一些POC。请注意,每个框架都有比下面所示更多的功能;为了清晰起见,我保持了每个例子的简洁。

最小起订量(自行决定)

如果您熟悉使用Moq框架,这是相对简单的。“诀窍”是在HttpClient内部模拟HttpMessageHandler——而不是HttpClient本身。注意:使用MockBehavior是一个很好的实践。严格模拟,以便提醒您没有显式模拟和预期的任何调用。

RichardSzalay。MockHttp

RichardSzalay。MockHttp是另一个流行的解决方案。我以前使用过这个,但发现它比Moq.Contrib.HttpClient稍微麻烦一些。这里可以使用两种不同的模式。Richard在这里描述了什么时候使用其中一个和另一个。

Moq.Contrib.HttpClient

就像使用Moq本身的解决方案一样,如果您熟悉使用Moq框架,这是很简单的。我发现这个解决方案更直接,代码更少。这是我选择使用的解决方案。注意,这个解决方案需要一个独立于Moq本身的Nuget - Moq. contrib . httpclient

WireMock。网

作为游戏的新手,WireMock.net越来越受欢迎。这将是一个合理的解决方案,而不是Microsoft.AspNetCore.TestHost,如果您正在编写集成测试,其中对端点的调用是实际执行的,而不是模拟的。一开始我以为这是我的选择,但出于两个原因决定放弃:

它实际上是开放端口以方便测试。由于我过去不得不修复由于HttpClient使用不当而导致的端口耗尽问题,所以我决定放弃这个解决方案,因为我不确定它在并行运行许多单元测试的大型代码库中是否能很好地扩展。 使用的url必须是可解析的(实际合法的url)。如果你想要简单的不关心一个“真正的”url(只是你期望的url实际上被调用),那么这可能不适合你。

例子

给定以下简单/做作的代码,下面是编写每个测试的方法。

public class ClassUnderTest
{
    private readonly HttpClient _httpClient;
    private const string Url = "https://myurl";

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

    public async Task<Person> GetPersonAsync(int id)
    {
        var response = await _httpClient.GetAsync($"{Url}?id={id}");
        return await response.Content.ReadFromJsonAsync<Person>();
    }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

最小起订量(自行决定)

[Fact]
public async Task JustMoq()
{
    //arrange
    const int personId = 1;
    var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockResponse = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = JsonContent.Create<Person>(dto)
    };

    mockHandler
        .Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
            ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(mockResponse);

    // Inject the handler or client into your application code
    var httpClient = new HttpClient(mockHandler.Object);
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    mockHandler.Protected().Verify(
        "SendAsync",
        Times.Exactly(1),
        ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
        ItExpr.IsAny<CancellationToken>());
}

RichardSzalay。MockHttp(使用BackendDefinition模式)

[Fact]
public async Task RichardSzalayMockHttpUsingBackendDefinition()
{
    //arrange
    const int personId = 1;
    using var mockHandler = new MockHttpMessageHandler();
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockResponse = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = JsonContent.Create<Person>(dto)
    };

    var mockedRequest = mockHandler.When(HttpMethod.Get, "https://myurl?id=1")
        .Respond(mockResponse.StatusCode, mockResponse.Content);

    // Inject the handler or client into your application code
    var httpClient = mockHandler.ToHttpClient();
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    Assert.Equivalent(dto, actual);
    Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
    mockHandler.VerifyNoOutstandingRequest();
}

RichardSzalay。MockHttp(使用RequestExpectation模式)

[Fact]
public async Task RichardSzalayMockHttpUsingRequestExpectation()
{
    //arrange
    const int personId = 1;
    using var mockHandler = new MockHttpMessageHandler();
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockResponse = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = JsonContent.Create<Person>(dto)
    };

    var mockedRequest = mockHandler.Expect(HttpMethod.Get, "https://myurl")
        .WithExactQueryString($"id={personId}")
        .Respond(mockResponse.StatusCode, mockResponse.Content);

    // Inject the handler or client into your application code
    var httpClient = mockHandler.ToHttpClient();
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    Assert.Equivalent(dto, actual);
    Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
    mockHandler.VerifyNoOutstandingExpectation();
}

Moq.Contrib.HttpClient

[Fact]
public async Task UsingMoqContribHttpClient()
{
    //arrange
    const int personId = 1;
    var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockUrl = $"https://myurl?id={personId}";
    var mockResponse = mockHandler.SetupRequest(HttpMethod.Get, mockUrl)
        .ReturnsJsonResponse<Person>(HttpStatusCode.OK, dto);

    // Inject the handler or client into your application code
    var httpClient = mockHandler.CreateClient();
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    Assert.Equivalent(dto, actual);
    mockHandler.VerifyRequest(HttpMethod.Get, mockUrl, Times.Once());
}

WireMock。网

public class TestClass : IDisposable
{
    private WireMockServer _server;

    public TestClass()
    {
        _server = WireMockServer.Start();
    }

    public void Dispose()
    {
        _server.Stop();
    }

    [Fact]
    public async Task UsingWireMock()
    {
        //arrange
        const int personId = 1;
        var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
        var mockUrl = $"https://myurl?id={personId}";

        _server.Given(
            Request.Create()
                .WithPath("/"))
            .RespondWith(
                Response.Create()
                    .WithStatusCode(200)
                    .WithHeader("Content-Type", "application/json")
                    .WithBodyAsJson(dto));

        // Inject the handler or client into your application code
        var httpClient = _server.CreateClient();
        var sut = new ClassUnderTest(httpClient);

        //act
        var actual = await sut.GetPersonAsync(personId);

        //assert
        Assert.NotNull(actual);
        Assert.Equivalent(dto, actual);
    }
}