我有一些问题,试图包装我的代码在单元测试中使用。问题在于。我有接口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。您应该注入一个负责为您提供HttpClient实例的工厂。这样你以后就可以模拟那个工厂并返回任何你想要的HttpClient(例如:一个模拟HttpClient而不是真正的HttpClient)。

所以,你会有一个这样的工厂:

public interface IHttpClientFactory
{
    HttpClient Create();
}

和一个实现:

public class HttpClientFactory
    : IHttpClientFactory
{
    public HttpClient Create()
    {
        var httpClient = new HttpClient();
        return httpClient;
    }
}

当然,你需要在你的IoC容器中注册这个实现。如果你使用Autofac,它会是这样的:

builder
    .RegisterType<IHttpClientFactory>()
    .As<HttpClientFactory>()
    .SingleInstance();

现在您将拥有一个适当的、可测试的实现。假设你的方法是这样的:

public class MyHttpClient
    : IMyHttpClient
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SalesOrderHttpClient(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<string> PostAsync(Uri uri, string content)
    {
        using (var client = _httpClientFactory.Create())
        {
            var clientAddress = uri.GetLeftPart(UriPartial.Authority);
            client.BaseAddress = new Uri(clientAddress);
            var content = new StringContent(content, Encoding.UTF8, "application/json");
            var uriAbsolutePath = uri.AbsolutePath;
            var response = await client.PostAsync(uriAbsolutePath, content);
            var responseJson = response.Content.ReadAsStringAsync().Result;
            return responseJson;
        }
    }
}

现在是测试部分。HttpClient扩展了抽象的HttpMessageHandler。让我们创建一个接受委托的HttpMessageHandler的“模拟”,这样当我们使用模拟时,我们也可以为每个测试设置每个行为。

public class MockHttpMessageHandler 
    : HttpMessageHandler
{
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;

    public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
    {
        _sendAsyncFunc = sendAsyncFunc;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _sendAsyncFunc.Invoke(request, cancellationToken);
    }
}

现在,在Moq(以及FluentAssertions,一个使单元测试更具可读性的库)的帮助下,我们拥有了对使用HttpClient的方法PostAsync进行单元测试所需的一切

public static class PostAsyncTests
{
    public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
        : Given_WhenAsync_Then_Test
    {
        private SalesOrderHttpClient _sut;
        private Uri _uri;
        private string _content;
        private string _expectedResult;
        private string _result;

        protected override void Given()
        {
            _uri = new Uri("http://test.com/api/resources");
            _content = "{\"foo\": \"bar\"}";
            _expectedResult = "{\"result\": \"ok\"}";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var messageHandlerMock =
                new MockHttpMessageHandler((request, cancellation) =>
                {
                    var responseMessage =
                        new HttpResponseMessage(HttpStatusCode.Created)
                        {
                            Content = new StringContent("{\"result\": \"ok\"}")
                        };

                    var result = Task.FromResult(responseMessage);
                    return result;
                });

            var httpClient = new HttpClient(messageHandlerMock);
            httpClientFactoryMock
                .Setup(x => x.Create())
                .Returns(httpClient);

            var httpClientFactory = httpClientFactoryMock.Object;

            _sut = new SalesOrderHttpClient(httpClientFactory);
        }

        protected override async Task WhenAsync()
        {
            _result = await _sut.PostAsync(_uri, _content);
        }


        [Fact]
        public void Then_It_Should_Return_A_Valid_JsonMessage()
        {
            _result.Should().BeEquivalentTo(_expectedResult);
        }
    }
}

显然这个测试很愚蠢,我们实际上是在测试我们的mock。但你懂的。您应该根据您的实现测试有意义的逻辑,例如..

如果响应的代码状态不是201,它是否应该抛出异常? 如果不能解析响应文本,应该发生什么? 等。

这个回答的目的是测试一些使用HttpClient的东西,这是一种很好的干净的方法。


更新 最近,我在测试中使用了一个http构建器,在那里我可以轻松地注入我所期望的json响应。

public class HttpClientBuilder
{
    private HttpMessageHandler _httpMessageHandler = new HttpClientHandler();
    
    public HttpClientBuilder WithJsonResponse(HttpStatusCode httpStatusCode, string json, string contentType = "application/json")
    {
        var mockHttpMessageHandler =
            new MockHttpMessageHandler(
                (request, cancellation) =>
                {
                    var responseMessage =
                        new HttpResponseMessage(httpStatusCode)
                        {
                            Content = new StringContent(json, Encoding.UTF8, contentType)
                        };
                    var result = Task.FromResult(responseMessage);
                    return result;
                });
        _httpMessageHandler = mockHttpMessageHandler;
        return this;
    }

    public HttpClient Build()
    {
        var httpClient = new HttpClient(_httpMessageHandler);
        return httpClient;
    }
}

class MockHttpMessageHandler 
    : HttpMessageHandler
{
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;

    public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
    {
        _sendAsyncFunc = sendAsyncFunc;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _sendAsyncFunc.Invoke(request, cancellationToken);
    }
}

因此,只要我有一个抽象的HttpClient,比如IHttpClientFactory,就像我上面建议的那样,在我的测试中,我可以做一些这样的事情

var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var jsonResponse = "{\"hello world\"}";
var httpClient = 
  new HttpClientBuilder()
    .WithJsonResponse(HttpStatusCode.OK, jsonResponse)
    .Build();
                
httpClientFactoryMock
  .Setup(x => x.Create())
  .Returns(httpClient);
var httpClientFactory = httpClientFactoryMock.Object;

然后使用httpClientFactory。

其他回答

我认为问题是你把它弄颠倒了。

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

    public AuroraClient() : this(new HttpClientHandler())
    {
    }

    public AuroraClient(HttpMessageHandler messageHandler)
    {
        _client = new HttpClient(messageHandler);
    }
}

如果你看了上面的类,我想这就是你想要的。微软建议保持客户端活跃以获得最佳性能,因此这种类型的结构允许您这样做。而且HttpMessageHandler是一个抽象类,因此是可嘲笑的。你的测试方法看起来像这样:

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var mockMessageHandler = new Mock<HttpMessageHandler>();
    // Set up your mock behavior here
    var auroraClient = new AuroraClient(mockMessageHandler.Object);
    // Act
    // Assert
}

这允许您在模拟HttpClient的行为时测试您的逻辑。

对不起,伙计们,在写完这篇文章并亲自尝试之后,我意识到您不能在HttpMessageHandler上模拟受保护的方法。我随后添加了以下代码,以允许注入适当的mock。

public interface IMockHttpMessageHandler
{
    Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    private readonly IMockHttpMessageHandler _realMockHandler;

    public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler)
    {
        _realMockHandler = realMockHandler;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _realMockHandler.SendAsync(request, cancellationToken);
    }
}

用它编写的测试看起来像下面这样:

[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
    // Arrange
    var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
    // Set up Mock behavior here.
    var client = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
    // Act
    // Assert
}

加入这个派对有点晚了,但我喜欢在带有下游REST依赖的dotnet核心微服务的集成测试中尽可能使用wiremocking (https://github.com/WireMock-Net/WireMock.Net)。

通过实现一个扩展IHttpClientFactory的TestHttpClientFactory,我们可以重写这个方法

HttpClient CreateClient(字符串名称)

所以当你在应用中使用命名客户端时,你可以控制返回一个连接到你的wiremock的HttpClient。

这种方法的好处是,您不会更改正在测试的应用程序中的任何内容,并且允许课程集成测试对您的服务执行实际的REST请求,并模拟实际下游请求应该返回的json(或任何东西)。这将导致在应用程序中进行简洁的测试和尽可能少的模拟。

    public class TestHttpClientFactory : IHttpClientFactory 
{
    public HttpClient CreateClient(string name)
    {
        var httpClient = new HttpClient
        {
            BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
            // G.Config is our singleton config access, so the endpoint 
            // to the running wiremock is used in the test
        };
        return httpClient;
    }
}

and

// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);

这是一个常见的问题,我非常希望能够模拟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是一种疯狂的时间浪费。

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

也许在您当前的项目中会有一些代码需要更改,但对于新项目,您绝对应该考虑使用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);
}

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

    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。保护包。