在单元测试中模拟HttpClient

时间:2016-04-05 11:23:54

标签: c# unit-testing moq

我在尝试将我的代码包装在单元测试中时遇到了一些问题。问题是这样的。我有接口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这样的东西。我该如何处理这个问题?

提前致谢。

22 个答案:

答案 0 :(得分:174)

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

如果您更喜欢DSL使用Moq,我在GitHub / Nuget上有一个库,可以让事情变得更轻松:https://github.com/richardszalay/mockhttp

var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localost/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'}

答案 1 :(得分:32)

我同意其他一些答案,最好的方法是模拟HttpMessageHandler,而不是包装HttpClient。这个答案的独特之处在于它仍然会注入HttpClient,允许它成为单例或通过依赖注入进行管理。

" HttpClient旨在实例化一次并在应用程序的整个生命周期中重复使用。" (Source)。

模拟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);
        }
    }
}

答案 2 :(得分:29)

您的接口公开具体的HttpClient类,因此使用此接口的任何类都与它绑定,这意味着它不能被模拟。

HttpClient不会从任何界面继承,因此您必须自己编写。我建议使用装饰器模式:

public interface IHttpHandler
{
    HttpResponseMessage Get(string url);
    HttpResponseMessage Post(string url, HttpContent content);
    Task<HttpResponseMessage> GetAsync(string url);
    Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}

你的班级将如下所示:

public class HttpClientHandler : IHttpHandler
{
    private HttpClient _client = new HttpClient();

    public HttpResponseMessage Get(string url)
    {
        return GetAsync(url).Result;
    }

    public HttpResponseMessage Post(string url, HttpContent content)
    {
        return PostAsync(url, content).Result;
    }

    public async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await _client.GetAsync(url);
    }

    public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
    {
        return await _client.PostAsync(url, content);
    }
}

所有这一切的要点是HttpClientHandler创建了自己的HttpClient,然后你可以创建多个以不同方式实现IHttpHandler的类。

这种方法的主要问题是你有效地编写了一个只调用另一个类中的方法的类,但是你可以从HttpClient创建一个继承的类(参见 Nkosi的例子,它比我的方法好得多。如果HttpClient有一个你可以模拟的界面,生活就会容易得多,不幸的是它没有。

然而,此示例金票。 IHttpHandler仍然依赖HttpResponseMessage,它属于System.Net.Http命名空间,因此如果您确实需要除HttpClient以外的其他实现,则必须执行某种映射才能转换他们对HttpResponseMessage个对象的回应。这当然只是一个问题如果您需要使用IHttpHandler的多个实现,但它看起来不像世界末日那样,但这是值得考虑的事情。

无论如何,您可以简单地模拟IHttpHandler,而不必担心具体的HttpClient类,因为它已被抽象出来。

我建议测试非异步方法,因为这些方法仍然会调用异步方法,但没有必要担心单元测试异步方法的麻烦,请参阅here

答案 3 :(得分:25)

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

我们经常在那里看到“Clients”,我们在代码中进行模拟,以便我们可以单独测试,因此我们会自动尝试将相同的原则应用于HttpClient。 HttpClient实际上做了很多;您可以将其视为HttpMessageHandler的管理器,因此您不想嘲笑它,这就是仍然没有接口的原因。您真正对单元测试或设计服务感兴趣的部分是HttpMessageHandler,因为这是返回响应的部分,而可以模拟它。

值得指出的是,你应该开始对待HttpClient就像一个更大的交易。例如:将新HttpClients的实例化保持在最低限度。重复使用它们,它们被设计为可以重复使用,并且如果你这样做,可以减少使用垃圾。如果你开始将它视为一个更大的交易,那么想要模拟它会更加错误,现在消息处理程序将开始成为你注入的东西,而不是客户端。

换句话说,围绕处理程序而不是客户端设计依赖项。更好的是,使用HttpClient的抽象“服务”允许您注入处理程序,并将其用作注入依赖项。然后在测试中,您可以伪造处理程序以控制设置测试的响应。

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

更新: 参见Joshua Dooms的例子。这正是我推荐的。

答案 4 :(得分:11)

基于其他答案,我建议使用此代码,它没有任何外部依赖关系:

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

答案 5 :(得分:9)

我认为问题在于你只是略微颠倒了。

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

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

如果你看一下上面的课程,我认为这就是你想要的。 Microsoft建议保持客户端活动以获得最佳性能,因此这种类型的结构允许您这样做。此外,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上模拟受保护的方法。我随后添加了以下代码以允许注入正确的模拟。

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 auroraClient = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
        // Act
        // Assert
    }

答案 6 :(得分:7)

我的一位同事注意到,大多数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) 类方法,包括常规

          +---+---+---+     +---+---+---+     +---+---+---+
list ---> |   |   | *-+---> |   |   | *-+---> |   |   | *-+--X
          |   | 1 |   |     |   | 2 |   |     |   | 3 |   |
       X--+-* |   |   | <---+-* |   |   | <---+-* |   |   |
          +---+---+---+     +---+---+---+     +---+---+---+

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

答案 7 :(得分:6)

一种替代方法是设置一个存根HTTP服务器,该服务器根据与请求网址匹配的模式返回预设响应,这意味着您测试真实的HTTP请求而不是模拟。从历史上看,这将需要大量的开发工作,并且考虑进行单元测试会慢得多,但是OSS库WireMock.net易于使用且速度足以运行大量测试,因此可能值得考虑。安装程序是几行代码:

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

您可以找到更多详情和guidance on using wiremock in tests here.

答案 8 :(得分:3)

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

如果使用 MOQ,正确的做法是将 using 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);

答案 9 :(得分:3)

参加聚会有点晚,但是我喜欢在具有下游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;
    }
}

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

答案 10 :(得分:2)

由于HttpClient使用SendAsync方法执行所有HTTP Requests,因此您可以使用override SendAsync方法并模拟HttpClient

对于从HttpClientinterface的自动换行,如下所示

public interface IServiceHelper
{
    HttpClient GetClient();
}

然后在interface上方用于服务中的依赖项注入,在下面的示例中

public class SampleService
{
    private readonly IServiceHelper serviceHelper;

    public SampleService(IServiceHelper serviceHelper)
    {
        this.serviceHelper = serviceHelper;
    }

    public async Task<HttpResponseMessage> Get(int dummyParam)
    {
        try
        {
            var dummyUrl = "http://www.dummyurl.com/api/controller/" + dummyParam;
            var client = serviceHelper.GetClient();
            HttpResponseMessage response = await client.GetAsync(dummyUrl);               

            return response;
        }
        catch (Exception)
        {
            // log.
            throw;
        }
    }
}

现在在单元测试项目中,创建一个用于模拟SendAsync的助手类。 这里是FakeHttpResponseHandler inheriting的{​​{1}}类,它将提供覆盖DelegatingHandler方法的选项。覆盖SendAsync方法后,需要为每个正在调用SendAsync方法的HTTP Request设置响应,为此需要创建一个SendAsync为{{1 }}和Dictionary设为key,这样只要有UrivalueHttpResponseMessage匹配,就会返回已配置的HTTP Request

Uri

通过模拟框架或类似的方法为SendAsync创建新的实现。 我们可以使用该HttpResponseMessage类来注入public class FakeHttpResponseHandler : DelegatingHandler { private readonly IDictionary<Uri, HttpResponseMessage> fakeServiceResponse; private readonly JavaScriptSerializer javaScriptSerializer; public FakeHttpResponseHandler() { fakeServiceResponse = new Dictionary<Uri, HttpResponseMessage>(); javaScriptSerializer = new JavaScriptSerializer(); } /// <summary> /// Used for adding fake httpResponseMessage for the httpClient operation. /// </summary> /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam> /// <param name="uri">Service end point URL.</param> /// <param name="httpResponseMessage"> Response expected when the service called.</param> public void AddFakeServiceResponse(Uri uri, HttpResponseMessage httpResponseMessage) { fakeServiceResponse.Remove(uri); fakeServiceResponse.Add(uri, httpResponseMessage); } /// <summary> /// Used for adding fake httpResponseMessage for the httpClient operation having query string parameter. /// </summary> /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam> /// <param name="uri">Service end point URL.</param> /// <param name="httpResponseMessage"> Response expected when the service called.</param> /// <param name="requestParameter">Query string parameter.</param> public void AddFakeServiceResponse<TQueryStringParameter>(Uri uri, HttpResponseMessage httpResponseMessage, TQueryStringParameter requestParameter) { var serilizedQueryStringParameter = javaScriptSerializer.Serialize(requestParameter); var actualUri = new Uri(string.Concat(uri, serilizedQueryStringParameter)); fakeServiceResponse.Remove(actualUri); fakeServiceResponse.Add(actualUri, httpResponseMessage); } // all method in HttpClient call use SendAsync method internally so we are overriding that method here. protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if(fakeServiceResponse.ContainsKey(request.RequestUri)) { return Task.FromResult(fakeServiceResponse[request.RequestUri]); } return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) { RequestMessage = request, Content = new StringContent("Not matching fake found") }); } } 类,以便无论何时由该IServiceHelper创建的FakeServiceHelper都将使用FakeHttpResponseHandler而不是实际的实现。

HttpClient

在测试中,通过添加class和预期的FakeHttpResponseHandler class来配置public class FakeServiceHelper : IServiceHelper { private readonly DelegatingHandler delegatingHandler; public FakeServiceHelper(DelegatingHandler delegatingHandler) { this.delegatingHandler = delegatingHandler; } public HttpClient GetClient() { return new HttpClient(delegatingHandler); } } FakeHttpResponseHandler class应该是实际的Uri端点HttpResponseMessage,以便从实际的Uri实现调用service方法时,它将与Uri相匹配overridden SendAsync中的内容,并以配置的service进行响应。 配置完成后,将Uri注入伪造的Dictionary实现中。 然后将HttpResponseMessage注入到实际服务中,这将使实际服务使用FakeHttpResponseHandler object方法。

IServiceHelper

GitHub Link : which is having sample implementation

答案 11 :(得分:1)

您所需要做的只是将HttpMessageHandler类的测试版本传递给HttpClient ctor。要点是,您的测试HttpMessageHandler类将有一个HttpRequestHandler委托,调用者可以设置该委托,并简单地按自己想要的方式处理HttpRequest

public class FakeHttpMessageHandler : HttpMessageHandler
    {
        public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
        (r, c) => 
            new HttpResponseMessage
            {
                ReasonPhrase = r.RequestUri.AbsoluteUri,
                StatusCode = HttpStatusCode.OK
            };


        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(HttpRequestHandler(request, cancellationToken));
        }
    }

您可以使用此类的实例来创建具体的HttpClient实例。通过HttpRequestHandler委托,您可以完全控制HttpClient发出的HTTP请求。

答案 12 :(得分:1)

Microsoft 现在建议使用 IHttpClientFactory 而不是直接使用 HttpClient

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0

带有返回预期结果的请求的示例 Mock:

private LoginController GetLoginController()
{
    var expected = "Hello world";
    var mockFactory = new Mock<IHttpClientFactory>();

    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(expected)
        });

    var httpClient = new HttpClient(mockMessageHandler.Object);

    mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);

    var logger = Mock.Of<ILogger<LoginController>>();

    var controller = new LoginController(logger, mockFactory.Object);

    return controller;
}

来源:

https://stackoverflow.com/a/66256132/3850405

答案 13 :(得分:1)

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

    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.Protected软件包。

答案 14 :(得分:1)

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

https://flurl.dev

这是.NET的HTTP客户端库,具有流畅的接口,该库专门启用了使用它发出HTTP请求的代码的可测试性。

网站上有很多代码示例,但简而言之,您可以在代码中像这样使用它。

添加用法。

using Flurl;
using Flurl.Http;

发送获取请求并阅读响应。

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

答案 15 :(得分:1)

许多答案都使我信服。

首先,假设您想对使用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);
        }
    }
}

显然,此测试很愚蠢,我们正在测试我们的模拟游戏。但是你明白了。您应该根据自己的实现来测试有意义的逻辑,例如。

  • 如果响应的代码状态不是201,是否应该引发异常?
  • 如果无法解析响应文本,应该怎么办?

这个答案的目的是测试使用HttpClient的东西,这是一种很好的清洁方式。

答案 16 :(得分:1)

就像在DI环境中一样,我做了非常简单的事情。

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(...);
    }

答案 17 :(得分:1)

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

  1. 在VS 2017中,右键单击System.Net.Http程序集并选择&#34; Add Fakes Assembly&#34;
  2. 将您的代码放在ShimsContext.Create()下的单元测试方法中。这样,您就可以隔离计划假冒HttpClient的代码。
  3. 取决于你的实现和测试,我建议实现你在HttpClient上调用方法的所有想要的动作,并想伪造返回的值。使用ShimHttpClient.AllInstances会在测试期间创建的所有实例中伪造您的实现。例如,如果要伪造GetAsync()方法,请执行以下操作:

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

答案 18 :(得分:0)

这是一个简单的解决方案,对我来说效果很好。

使用moq模拟库。

// ARRANGE
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock
   .Protected()
   // Setup the PROTECTED method to mock
   .Setup<Task<HttpResponseMessage>>(
      "SendAsync",
      ItExpr.IsAny<HttpRequestMessage>(),
      ItExpr.IsAny<CancellationToken>()
   )
   // prepare the expected response of the mocked http call
   .ReturnsAsync(new HttpResponseMessage()
   {
      StatusCode = HttpStatusCode.OK,
      Content = new StringContent("[{'id':1,'value':'1'}]"),
   })
   .Verifiable();

// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
   BaseAddress = new Uri("http://test.com/"),
};

var subjectUnderTest = new MyTestClass(httpClient);

// ACT
var result = await subjectUnderTest
   .GetSomethingRemoteAsync('api/test/whatever');

// ASSERT
result.Should().NotBeNull(); // this is fluent assertions here...
result.Id.Should().Be(1);

// also check the 'http' call was like we expected it
var expectedUri = new Uri("http://test.com/api/test/whatever");

handlerMock.Protected().Verify(
   "SendAsync",
   Times.Exactly(1), // we expected a single external request
   ItExpr.Is<HttpRequestMessage>(req =>
      req.Method == HttpMethod.Get  // we expected a GET request
      && req.RequestUri == expectedUri // to this uri
   ),
   ItExpr.IsAny<CancellationToken>()
);

来源:https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/

答案 19 :(得分:0)

PointZeroTwo's answer的启发,下面是使用NUnitFakeItEasy的示例。

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(...);
    }
}

答案 20 :(得分:0)

加上我的2美分。为了模拟特定的HTTP请求方法,可以使用Get或Post。这对我有用。

mockHttpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(a => a.Method == HttpMethod.Get), ItExpr.IsAny<CancellationToken>())
                                                .Returns(Task.FromResult(new HttpResponseMessage()
                                                {
                                                    StatusCode = HttpStatusCode.OK,
                                                    Content = new StringContent(""),
                                                })).Verifiable();

答案 21 :(得分:0)

如果您不介意运行自己的 http 服务器,可以尝试 Xim。就这么简单:

using Xim.Simulators.Api;
[Test]
public async Task TestHttpGetMethod()
{
    using var simulation = Simulation.Create();
    using var api = simulation
        .AddApi()
        .AddHandler("GET /books/1234", ApiResponse.Ok())
        .Build();
    await api.StartAsync();
    var httpClient = new HttpClient();

    var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{api.Location}/books/1234"));

    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
    Assert.IsTrue(api.ReceivedApiCalls.Any(call => call.Action == "GET /books/1234"));
}

这是使用模拟的一个不错的替代方案,并且可能在某些情况下满足您的需求。它建立在 Kestrel 之上(是的,我就是作者)。