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


当前回答

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

其他回答

一种替代方法是设置一个存根HTTP服务器,它根据与请求url匹配的模式返回罐装响应,这意味着您要测试真正的HTTP请求,而不是模拟。从历史上看,这将花费大量的开发工作,并将远远慢到考虑进行单元测试,然而,OSS库WireMock.net易于使用,足够快,可以运行大量的测试,所以可能值得考虑。Setup是几行代码:

var server = FluentMockServer.Start();
server.Given(
      Request.Create()
      .WithPath("/some/thing").UsingGet()
   )
   .RespondWith(
       Response.Create()
       .WithStatusCode(200)
       .WithHeader("Content-Type", "application/json")
       .WithBody("{'attr':'value'}")
   );

您可以在这里找到在测试中使用wiremock的更多细节和指导。

我的一个同事注意到,大多数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

不要有一个包装器来创建一个新的HttpClient实例。如果您这样做,您将在运行时耗尽套接字(即使您正在处理HttpClient对象)。

如果使用MOQ,正确的做法是添加使用MOQ . protected;到您的测试,然后编写如下代码:

var response = new HttpResponseMessage(HttpStatusCode.OK)
{
    Content = new StringContent("It worked!")
};
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
    .Protected()
    .Setup<Task<HttpResponseMessage>>(
        "SendAsync",
        ItExpr.IsAny<HttpRequestMessage>(),
        ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(() => response);


var httpClient = new HttpClient(mockHttpMessageHandler.Object);

很多答案我都不相信。

首先,假设您想要对一个使用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。

加入这个派对有点晚了,但我喜欢在带有下游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);