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


当前回答

这是一个老问题,但我有一种冲动,想用一个我在这里没有看到的解决方案来扩展答案。 您可以伪造Microsoft程序集(System.Net.Http),然后在测试期间使用ShinsContext。

In VS 2017, right click on the System.Net.Http assembly and choose "Add Fakes Assembly" Put your code in the unit test method under a ShimsContext.Create() using. This way, you can isolate the code where you are planning to fake the HttpClient. Depends on your implementation and test, I would suggest to implement all the desired acting where you call a method on the HttpClient and want to fake the returned value. Using ShimHttpClient.AllInstances will fake your implementation in all the instances created during your test. For example, if you want to fake the GetAsync() method, do the following: [TestMethod] public void FakeHttpClient() { using (ShimsContext.Create()) { System.Net.Http.Fakes.ShimHttpClient.AllInstances.GetAsyncString = (c, requestUri) => { //Return a service unavailable response var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable); var task = Task.FromResult(httpResponseMessage); return task; }; //your implementation will use the fake method(s) automatically var client = new Connection(_httpClient); client.doSomething(); } }

其他回答

一种替代方法是设置一个存根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的方法进行单元测试。您不应该在实现中直接实例化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。

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

[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);
    }
}

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

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