在单元测试中模拟HttpClient


110

我在尝试包装要在单元测试中使用的代码时遇到一些问题。问题是这样的。我有接口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之类的东西。我应该如何解决这个问题?

提前致谢。


1
问题出HttpClient在哪里?您正在强迫客户使用HttpClient具体的类。相反,你应该暴露一个抽象HttpClient
Mike Eason

您能否更深入地解释它?我应该如何构建连接类构造函数,因为我不希望其他类中的HttpClient依赖关系使用Connection类。例如,我不想在Connection的构造函数中传递简短的HttpClient,因为这会使使用Connection的其他所有类都依赖于HttpClient?
tjugg

出于兴趣,您用谷歌搜索了什么?显然,mockhttp可以使用一些SEO改进。
理查德·萨雷

@Mike-正如我的回答中所述,确实不需要抽象HttpClient。可以完全按原样测试。我有许多项目都使用此方法的无后端测试套件。
理查德·萨雷

Answers:


37

您的接口公开了具体的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类的抽象化。

我建议测试非异步方法,因为它们仍然调用异步方法,而不必担心对异步方法进行单元测试的麻烦,请参见此处


这确实回答了我的问题。Nkosis的答案也是正确的,因此我不确定应该接受哪个答案,但是我会继续回答。谢谢你们的努力
tjugg '16

@tjugg很高兴提供帮助。如果发现有用的答案,请随意投票。
Nkosi

3
值得注意的是,此答​​案与Nkosi答案之间的主要区别在于,它是一个更薄的抽象。薄可能是一个不起眼的物体的
Ben Aaronson

227

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
所以我只是将MockHttpMessageHandler传递为messagehandler Httphandler类?或者您如何在自己的项目中实现它
tjugg 2016年

2
很好的答案,我最初不会知道。使使用HttpClient的工作不是那么糟糕。
Bealer

6
对于那些不想处理注入客户但仍然想要简单的可测试性的人来说,实现它是微不足道的。只需更换var client = new HttpClient()var client = ClientFactory()和安装现场internal static Func<HttpClient> ClientFactory = () => new HttpClient();,并在测试级别,你可以重写这个领域。
克里斯·马里西克

3
@ChrisMarisic,您建议使用一种服务地点形式来代替注射。服务位置是众所周知的反模式,因此imho注入是可取的。
MarioDS '17

2
@MarioDS,无论如何,您根本不应该注入HttpClient 实例。如果您对此一无所知,请使用HttpClientFactory象中的那样注入a Func<HttpClient>。鉴于我将HttpClient视为纯粹的实现细节而不是依赖项,因此如上所述,我将使用静态变量。我对操纵内部的测试完全满意。如果我在乎纯粹主义,那么我将站在完整的服务器上并测试实时代码路径。使用任何一种模拟都意味着您接受近似的行为而不是实际的行为。
克里斯·马里西克

39

我同意其他一些答案,最好的方法是模拟HttpMessageHandler而不是包装HttpClient。这个答案是唯一的,因为它仍然会注入HttpClient,从而使其成为单例或通过依赖项注入进行管理。

“ HttpClient旨在实例化一次,并在应用程序的整个生命周期内重复使用。” (来源)。

因为SendAsync受保护,所以模拟HttpMessageHandler可能会有些棘手。这是使用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);
        }
    }
}

1
我真的很喜欢这个。该mockMessageHandler.Protected()是杀手。感谢您的示例。它允许编写测试而无需修改源代码。
tyrion

1
仅供参考,Moq 4.8支持受保护成员的强类型模拟-github.com/Moq/moq4/wiki/Quickstart
Richard

2
这看起来很棒。Moq还支持ReturnAsync,因此代码看起来像.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord

谢谢@kord,我在回答中补充了该点
PointZeroTwo '18

3
有什么方法可以验证是否使用某些参数调用了“ SandAsync”?我尝试使用... Protected()。Verify(...),但看起来它不适用于异步方法。
罗曼

29

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

我们经常在其中看到我们在代码中模拟的“客户端”,以便我们可以进行隔离测试,因此我们自动尝试将相同的原理应用于HttpClient。HttpClient实际上做了很多事情;您可以将其视为HttpMessageHandler的管理器,所以您不想模拟它,这就是为什么它仍然没有接口的原因。您甚至对单元测试或设计服务真正感兴趣的部分就是HttpMessageHandler,因为它是返回响应的内容,您可以对此进行模拟。

还值得指出的是,您可能应该开始将HttpClient视为更大的交易。例如:尽量减少对新HttpClient的描述。重复使用它们,它们被设计为可以重复使用,并且如果您这样做的话,可以减少浪费的资源。如果您开始将其视为更大的交易,那么想要模拟它会感觉更加错误,现在消息处理程序将开始成为您要注入的东西,而不是客户端。

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

包装HttpClient会浪费时间。

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


17

正如评论中提到的那样,您需要抽象出a 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专门针对该功能的服务。使用抽象是一种干净的方法(我认为)并且是SOLID方法,如果随着框架的变化需要将底层客户端切换到其他位置,则可以使代码更易于维护。

以下是实现方式的摘要。

/// <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和使用来创建您的特定实例的适配器HttpClientFactory。这样做使测试超出HTTP请求的逻辑变得微不足道,这是这里的目标。
pimbrouwers

13

在其他答案的基础上,我建议使用以下代码,该代码没有任何外部依赖项:

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

4
您正在有效地测试您的模拟。模拟的真正力量在于您可以设置期望值并在每次测试中更改其行为。实际上,您必须HttpMessageHandler自己实现一些功能,这几乎是不可能的,并且必须这样做,因为方法是正确的protected internal
MarioDS

3
@MarioDS我认为关键是您可以模拟HTTP响应,以便可以测试其余代码。如果注入的工厂可以获取HttpClient,则在测试中可以提供此HttpClient。
chris313​​89 '19

13

我认为问题是您的功能有些颠倒了。

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

9

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


1
但是,这不适用于任何SendAsync(HttpRequestMessage)直接调用的代码。如果您可以修改代码以不使用此便利功能,那么直接通过重写SendAsync来模拟HttpClient 实际上是我找到的最干净的解决方案。
迪伦·尼科尔森

8

一种替代方法是设置一个存根HTTP服务器,该服务器基于匹配请求url的模式返回罐装响应,这意味着您测试的是真实的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'}")
   );

您可以在此处找到有关在测试中使用Wiremock的更多详细信息和指南。


8

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

使用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/


我也成功地使用了它。我更喜欢这种方式,而不是增加对nuget的依赖,实际上,您实际上也了解到了幕后的情况。令人高兴的是,大多数方法最终仍会使用SendAsync,因此不需要额外的设置。
史蒂夫·佩蒂弗

4

许多答案都使我信服。

首先,假设您想对使用的方法进行单元测试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;
        }
    }
}

现在是测试部分。HttpClientextend 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,一个使单元测试更具可读性的库)的帮助,我们拥有了对单元测试使用的方法PostAsync进行单元测试所需的一切 HttpClient

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的东西,这是一种很好的清洁方法。


4

加入聚会有点晚,但是我喜欢使用Wiremocking(https://github.com/WireMock-Net/WireMock.Net在具有下游REST依赖项的dotnet核心微服务的集成测试中尽可能)。

通过实现扩展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);

2

由于HttpClient使用SendAsyncmethod来执行所有操作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 DelegatingHandler它将提供一个替代SendAsync方法的选项。重写后SendAsync的方法需要设定每个响应HTTP Request该调用SendAsync方法,为创造一个Dictionarykey作为Urivalue作为HttpResponseMessage等等,只要有一个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实施其将匹配UriDictionary与配置的响应HttpResponseMessage。配置完成后,将注入FakeHttpResponseHandler object伪造的IServiceHelper实现。然后将注入FakeServiceHelper class到实际服务中,这将使实际服务使用该override SendAsync方法。

[TestClass]
public class SampleServiceTest
{
    private FakeHttpResponseHandler fakeHttpResponseHandler;

    [TestInitialize]
    public void Initialize()
    {
        fakeHttpResponseHandler = new FakeHttpResponseHandler();
    }

    [TestMethod]
    public async Task GetMethodShouldReturnFakeResponse()
    {
        Uri uri = new Uri("http://www.dummyurl.com/api/controller/");
        const int dummyParam = 123456;
        const string expectdBody = "Expected Response";

        var expectedHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(expectdBody)
        };

        fakeHttpResponseHandler.AddFakeServiceResponse(uri, expectedHttpResponseMessage, dummyParam);

        var fakeServiceHelper = new FakeServiceHelper(fakeHttpResponseHandler);

        var sut = new SampleService(fakeServiceHelper);

        var response = await sut.Get(dummyParam);

        var responseBody = await response.Content.ReadAsStringAsync();

        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual(expectdBody, responseBody);
    }
}

GitHub链接:具有示例实现


尽管这段代码可以解决问题,但包括解释如何以及为何解决该问题的说明,确实可以帮助提高您的帖子质量,并可能导致更多的投票。请记住,您将来会为读者回答问题,而不仅仅是现在问的人。请编辑您的答案以添加说明,并指出适用的限制和假设。
32БогданОпир

感谢@БогданОпир反馈更新的解释。
ghosh-arun

1

您可以使用RichardSzalay MockHttp库,该库模拟HttpMessageHandler并可以返回要在测试期间使用的HttpClient对象。

GitHub MockHttp

PM>安装包RichardSzalay.MockHttp

从GitHub文档

MockHttp定义了替换的HttpMessageHandler,它是驱动HttpClient的引擎,提供了流畅的配置API并提供了罐头响应。调用方(例如,应用程序的服务层)仍然不知道它的存在。

来自GitHub的示例

 var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localhost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();

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

这是一个古老的问题,但我感到有必要通过在这里没有看到的解决方案来扩展答案。
您可以伪造Microsoft组件(System.Net.Http),然后在测试期间使用ShinsContext。

  1. 在VS 2017中,右键单击System.Net.Http程序集,然后选择“添加假程序集”
  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(); 
        }
    }

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

1

您所需要的只是HttpMessageHandler传递给HttpClientctor 的类的测试版本。要点是,您的测试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请求。


1

PointZeroTwo答案的启发,这是使用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(...);
    }
}

如果我想将参数传递给方法,该方法针对“ x.Method.Name” ..提到?
Shailesh

0

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

0

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

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


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();
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.