在单元测试中模拟HttpClient

tju*_*ugg 90 c# unit-testing moq

我有一些问题试图包装我的代码用于单元测试.问题是这样的.我有接口IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}
Run Code Online (Sandbox Code Playgroud)

而使用它的类,HttpHandler:

public class HttpHandler : IHttpHandler
{
    public HttpClient client
    {
        get
        {
            return new HttpClient();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后是Connection类,它使用simpleIOC来注入客户端实现:

public class Connection
{
    private IHttpHandler _httpClient;

    public Connection(IHttpHandler httpClient)
    {
        _httpClient = httpClient;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我有一个单元测试项目,有这个类:

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?     

}
Run Code Online (Sandbox Code Playgroud)

现在显然我将在Connection类中有方法从后端检索数据(JSON).但是,我想为这个类编写单元测试,显然我不想针对真正的后端编写测试,而是模拟测试.我试图谷歌一个很好的答案,但没有取得很大的成功.我之前可以使用Moq进行模拟,但从不使用像httpClient这样的东西.我该如何处理这个问题?

提前致谢.

Ric*_*lay 191

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'}
Run Code Online (Sandbox Code Playgroud)

  • 对于那些不想处理注入客户但仍想要易于测试的人来说,实现这一目标是微不足道的.只需用`var client = ClientFactory()`替换`var client = new HttpClient()`并设置一个字段`internal static Func <HttpClient> ClientFactory =()=> new HttpClient();`并且在测试级别你可以重写这个字段. (6认同)
  • @ChrisMarisic 作为更新,现在明确建议您使用 HttpClient 的单个实例 (4认同)
  • 此策略在 Moq-land 中:https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/ (4认同)
  • 伟大的答案和我最初不会知道的事情.使用HttpClient并不是那么糟糕. (2认同)
  • @ChrisMarisic,您建议使用一种服务地点形式来代替注射。服务位置是众所周知的反模式,因此imho注入是可取的。 (2认同)
  • @MarioDS 服务位置(SL)是一个很好的模式。它绝对不是一种反模式,事实上,服务位置是现有的最强大的模式之一。与所有模式一样,它也可能被错误地使用,考虑到 SL 提供的动态性,如果应用不当,它可能会变得笨拙。几乎每个 IOC 容器都依赖于 SL 模式。五年多以来,我一直坚持这个立场 http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ Seemann 可以犯错。 (2认同)
  • @MarioDS,无论如何,你根本不应该注入一个HttpClient**实例**.如果你已经设置了使用constructer注入,那么你应该在`Func <HttpClient>`中注入一个`HttpClientFactory`.鉴于我认为HttpClient纯粹是一个实现细节而不是依赖,我将使用静态,就像我上面说明的那样.操作内部的测试我完全没问题.如果我关心纯粹主义,我将站起来完整的服务器并测试实时代码路径.使用任何类型的模拟意味着您接受行为的近似而不是实际行为. (2认同)
  • 如果您让依赖注入处理生命周期,那么您可以注入“HttpClient”的实例。这通常是通过“IServiceCollection.AddHttpClient”完成的。DI 将在幕后使用“HttpClientFactory”并适当地处理“HttpClient”。 (2认同)

Poi*_*Two 33

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

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

模拟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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我真的很喜欢这个。`mockMessageHandler.Protected()` 是杀手锏。感谢这个例子。它允许在不修改源代码的情况下编写测试。 (3认同)
  • 仅供参考,Moq 4.8 支持受保护成员的强类型模拟 - https://github.com/Moq/moq4/wiki/Quickstart (2认同)
  • 这看起来很棒。Moq也支持ReturnAsync,因此代码看起来像是`.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK,Content = new StringContent(testContent)})) (2认同)
  • 有没有办法验证用一些参数调用"SandAsync"?我试过使用... Protected().验证(...),但看起来它不适用于异步方法. (2认同)
  • @Rroman 使用回调来捕获输入参数。异步方法的技巧是你必须从它们返回一些东西: .Setup(....).Callback(...).Returns(Task.FromResult(...)) (2认同)

Mik*_*son 31

您的接口公开具体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);
}
Run Code Online (Sandbox Code Playgroud)

你的班级将如下所示:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

这个例子不是金票.IHttpHandler仍然依赖于HttpResponseMessage属于System.Net.Http命名空间的,因此如果您确实需要其他实现HttpClient,则必须执行某种映射以将其响应转换为HttpResponseMessage对象.这当然只是一个问题,如果你需要使用多个实现,IHttpHandler但它看起来不像你这样做,它不是世界末日,但它是值得思考的东西.

无论如何,你可以简单地模拟,IHttpHandler而不必担心具体的HttpClient类,因为它已经被抽象掉了.

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

  • 值得注意的是,这个答案与Nkosi之间的主要区别在于它是一个更薄的抽象.瘦弱可能对[不起眼的对象]有用(http://stackoverflow.com/questions/5324049/what-is-the-humble-object-pattern-and-when-is-it-useful) (3认同)

Dav*_*ack 28

有几种不同的方法来模拟HttpClient. xUnit在决定单一解决方案之前,我已经使用了以下一些 POC Moq.Contrib.HttpClient。请注意,每个框架的功能比我下面展示的要多得多;为了清晰起见,我使每个示例都简洁明了。

起订量(单独)

如果您熟悉使用Moq框架,这相对简单。“技巧”是嘲笑HttpMessageHandler内部HttpClient——而不是HttpClient本身。注意:在模拟中使用是一个很好的做法MockBehavior.Strict,这样您就可以收到任何未明确模拟且预期的调用的警报。

RichardSzalay.MockHttp

RichardSzalay.MockHttp是另一个流行的解决方案。我过去用过这个,但发现它比Moq.Contrib.HttpClient. 这里可以使用两种不同的模式。Richard 描述了何时使用其中一种与另一种。

Moq.Contrib.HttpClient

与单独使用的解决方案一样Moq,如果您熟悉使用Moq框架,那么这很简单。我发现这个解决方案稍微更直接,代码更少。这是我选择使用的解决方案。请注意,此解决方案需要一个独立的 Nuget Moq- Moq.Contrib.HttpClient

WireMock.Net

WireMock.net作为该游戏的新成员,越来越受欢迎。这将是一个合理的解决方案,而不是Microsoft.AspNetCore.TestHost如果您正在编写集成测试,其中实际调用端点而不是被模拟。我一开始认为这会是我的选择,但出于两个原因决定不这样做:

  1. 它确实打开端口以方便测试。由于我必须解决过去因使用不当而导致的端口耗尽HttpClient问题,因此我决定放弃此解决方案,因为我不确定它在并行运行许多单元测试的大型代码库中的扩展效果如何。
  2. 使用的 url 必须是可解析的(实际合法的 url)。如果您想要简单而不关心“真实”url(只是您期望的 url 实际上被调用),那么这可能不适合您。

例子

给定以下简单/人为的代码,您将如何编写每个测试。

public class ClassUnderTest
{
    private readonly HttpClient _httpClient;
    private const string Url = "https://myurl";

    public ClassUnderTest(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Person> GetPersonAsync(int id)
    {
        var response = await _httpClient.GetAsync($"{Url}?id={id}");
        return await response.Content.ReadFromJsonAsync<Person>();
    }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

起订量(单独)

[Fact]
public async Task JustMoq()
{
    //arrange
    const int personId = 1;
    var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockResponse = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = JsonContent.Create<Person>(dto)
    };

    mockHandler
        .Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
            ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(mockResponse);

    // Inject the handler or client into your application code
    var httpClient = new HttpClient(mockHandler.Object);
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    mockHandler.Protected().Verify(
        "SendAsync",
        Times.Exactly(1),
        ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
        ItExpr.IsAny<CancellationToken>());
}
Run Code Online (Sandbox Code Playgroud)

RichardSzalay.MockHttp(使用 BackendDefinition 模式)

[Fact]
public async Task RichardSzalayMockHttpUsingBackendDefinition()
{
    //arrange
    const int personId = 1;
    using var mockHandler = new MockHttpMessageHandler();
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockResponse = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = JsonContent.Create<Person>(dto)
    };

    var mockedRequest = mockHandler.When(HttpMethod.Get, "https://myurl?id=1")
        .Respond(mockResponse.StatusCode, mockResponse.Content);

    // Inject the handler or client into your application code
    var httpClient = mockHandler.ToHttpClient();
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    Assert.Equivalent(dto, actual);
    Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
    mockHandler.VerifyNoOutstandingRequest();
}
Run Code Online (Sandbox Code Playgroud)

RichardSzalay.MockHttp(使用RequestExpectation模式)

[Fact]
public async Task RichardSzalayMockHttpUsingRequestExpectation()
{
    //arrange
    const int personId = 1;
    using var mockHandler = new MockHttpMessageHandler();
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockResponse = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = JsonContent.Create<Person>(dto)
    };

    var mockedRequest = mockHandler.Expect(HttpMethod.Get, "https://myurl")
        .WithExactQueryString($"id={personId}")
        .Respond(mockResponse.StatusCode, mockResponse.Content);

    // Inject the handler or client into your application code
    var httpClient = mockHandler.ToHttpClient();
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    Assert.Equivalent(dto, actual);
    Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
    mockHandler.VerifyNoOutstandingExpectation();
}
Run Code Online (Sandbox Code Playgroud)

Moq.Contrib.HttpClient

[Fact]
public async Task UsingMoqContribHttpClient()
{
    //arrange
    const int personId = 1;
    var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockUrl = $"https://myurl?id={personId}";
    var mockResponse = mockHandler.SetupRequest(HttpMethod.Get, mockUrl)
        .ReturnsJsonResponse<Person>(HttpStatusCode.OK, dto);

    // Inject the handler or client into your application code
    var httpClient = mockHandler.CreateClient();
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    Assert.Equivalent(dto, actual);
    mockHandler.VerifyRequest(HttpMethod.Get, mockUrl, Times.Once());
}
Run Code Online (Sandbox Code Playgroud)

WireMock.NET

public class TestClass : IDisposable
{
    private WireMockServer _server;

    public TestClass()
    {
        _server = WireMockServer.Start();
    }

    public void Dispose()
    {
        _server.Stop();
    }

    [Fact]
    public async Task UsingWireMock()
    {
        //arrange
        const int personId = 1;
        var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
        var mockUrl = $"https://myurl?id={personId}";

        _server.Given(
            Request.Create()
                .WithPath("/"))
            .RespondWith(
                Response.Create()
                    .WithStatusCode(200)
                    .WithHeader("Content-Type", "application/json")
                    .WithBodyAsJson(dto));

        // Inject the handler or client into your application code
        var httpClient = _server.CreateClient();
        var sut = new ClassUnderTest(httpClient);

        //act
        var actual = await sut.GetPersonAsync(personId);

        //assert
        Assert.NotNull(actual);
        Assert.Equivalent(dto, actual);
    }
}
Run Code Online (Sandbox Code Playgroud)


Sin*_*tic 27

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

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

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

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

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

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


j7n*_*n7k 18

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

使用 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>()
);
Run Code Online (Sandbox Code Playgroud)

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


Nko*_*osi 14

正如评论中所提到的,你需要抽象出来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);
}
Run Code Online (Sandbox Code Playgroud)

如前所述,这是出于特定目的.我完全抽象了大多数依赖关系到任何处理HttpClient和关注我想要返回的东西.您应该评估要抽象的方式HttpClient,以便仅提供所需的必要功能.

现在,这将允许您仅模拟需要测试的内容.

我甚至建议IHttpHandler完全废除并使用HttpClient抽象IHttpClient.但我只是不挑选,因为你可以用抽象客户端的成员替换你的处理程序接口的主体.

IHttpClient然后,可以使用can 的实现来包装/调整实际/具体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
}
Run Code Online (Sandbox Code Playgroud)

正如您在上面的示例中所看到的,通常与使用相关的许多繁重工作HttpClient隐藏在抽象之后.

然后,您可以使用抽象的客户端注入连接类

public class Connection
{
    private IHttpClient _httpClient;

    public Connection(IHttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,您的测试可以模拟您的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();  
}
Run Code Online (Sandbox Code Playgroud)


piu*_*ius 12

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

[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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 你正在有效地测试你的模拟.模拟的真正力量在于您可以设置期望并在每次测试中改变它的行为.你必须自己实现一些`HttpMessageHandler`这一事实使得接下来是不可能的 - 你必须这样做,因为这些方法是`protected internal`. (4认同)
  • @MarioDS我认为关键是您可以模拟HTTP响应,以便可以测试其余代码。如果注入的工厂可以获取HttpClient,则在测试中可以提供此HttpClient。 (2认同)

Jos*_*oms 12

我认为问题在于你只是有点颠倒了.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

    public AuroraClient(HttpMessageHandler messageHandler)
    {
        _client = new HttpClient(messageHandler);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你看一下上面的课程,我认为这就是你想要的.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
}
Run Code Online (Sandbox Code Playgroud)

这允许您在模拟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);
    }
}
Run Code Online (Sandbox Code Playgroud)

用这个编写的测试看起来如下所示:

[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
    }
Run Code Online (Sandbox Code Playgroud)


Ogg*_*las 9

Microsoft 现在提供了使用 aIHttpClientFactory而不是直接使用 的替代方案HttpClient

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

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

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;
}
Run Code Online (Sandbox Code Playgroud)

来源:

/sf/answers/4637929271/

  • 微软还建议使用命名和类型的HttpClients,https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests该语句不支持你的回答 (2认同)

Ada*_*dam 8

我的一位同事注意到大多数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);
Run Code Online (Sandbox Code Playgroud)

并且您的代码可以调用大多数(但不是全部)HttpClient类方法,包括常规方法

httpClient.SendAsync(req)
Run Code Online (Sandbox Code Playgroud)

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


Pet*_*ris 8

不要有创建 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);
Run Code Online (Sandbox Code Playgroud)


ala*_*ree 7

一种替代方法是设置一个存根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'}")
   );
Run Code Online (Sandbox Code Playgroud)

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


Mar*_*oss 6

加入聚会有点晚,但我喜欢在具有下游 REST 依赖项的 dotnet 核心微服务的集成测试中尽可能使用线模拟 ( 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);
Run Code Online (Sandbox Code Playgroud)


ibe*_*dev 5

很多答案我都不相信。

首先,假设您要对使用HttpClient. 您不应HttpClient直接在您的实现中实例化。你应该注入一个工厂,负责HttpClient为你提供一个实例。这样你可以稍后在那个工厂模拟并返回HttpClient你想要的任何东西(例如:模拟HttpClient而不是真实的)。

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

public interface IHttpClientFactory
{
    HttpClient Create();
}
Run Code Online (Sandbox Code Playgroud)

和一个实现:

public class HttpClientFactory
    : IHttpClientFactory
{
    public HttpClient Create()
    {
        var httpClient = new HttpClient();
        return httpClient;
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,您需要在您的 IoC 容器中注册此实现。如果您使用 Autofac,它将类似于:

builder
    .RegisterType<IHttpClientFactory>()
    .As<HttpClientFactory>()
    .SingleInstance();
Run Code Online (Sandbox Code Playgroud)

现在您将拥有一个适当且可测试的实现。想象一下你的方法是这样的:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在是测试部分。HttpClientextends 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,在 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

显然这个测试很愚蠢,我们真的在测试我们的模拟。但是你明白了。您应该根据您的实现测试有意义的逻辑,例如..

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

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