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


当前回答

有几种不同的方法来模拟HttpClient。以下是我在决定使用单一解决方案(Moq.Contrib.HttpClient)之前对xUnit做的一些POC。请注意,每个框架都有比下面所示更多的功能;为了清晰起见,我保持了每个例子的简洁。

最小起订量(自行决定)

如果您熟悉使用Moq框架,这是相对简单的。“诀窍”是在HttpClient内部模拟HttpMessageHandler——而不是HttpClient本身。注意:使用MockBehavior是一个很好的实践。严格模拟,以便提醒您没有显式模拟和预期的任何调用。

RichardSzalay。MockHttp

RichardSzalay。MockHttp是另一个流行的解决方案。我以前使用过这个,但发现它比Moq.Contrib.HttpClient稍微麻烦一些。这里可以使用两种不同的模式。Richard在这里描述了什么时候使用其中一个和另一个。

Moq.Contrib.HttpClient

就像使用Moq本身的解决方案一样,如果您熟悉使用Moq框架,这是很简单的。我发现这个解决方案更直接,代码更少。这是我选择使用的解决方案。注意,这个解决方案需要一个独立于Moq本身的Nuget - Moq. contrib . httpclient

WireMock。网

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

它实际上是开放端口以方便测试。由于我过去不得不修复由于HttpClient使用不当而导致的端口耗尽问题,所以我决定放弃这个解决方案,因为我不确定它在并行运行许多单元测试的大型代码库中是否能很好地扩展。 使用的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; }
}

最小起订量(自行决定)

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

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

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

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

WireMock。网

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

其他回答

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

[TestClass]
public class MyTestClass
{
    [TestMethod]
    public async Task MyTestMethod()
    {
        var httpClient = new HttpClient(new MockHttpMessageHandler());

        var content = await httpClient.GetStringAsync("http://some.fake.url");

        Assert.AreEqual("Content as string", content);
    }
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Content as string")
        };

        return await Task.FromResult(responseMessage);
    }
}

我同意其他一些回答,最好的方法是在HttpClient内部模拟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);
        }
    }
}

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

有几种不同的方法来模拟HttpClient。以下是我在决定使用单一解决方案(Moq.Contrib.HttpClient)之前对xUnit做的一些POC。请注意,每个框架都有比下面所示更多的功能;为了清晰起见,我保持了每个例子的简洁。

最小起订量(自行决定)

如果您熟悉使用Moq框架,这是相对简单的。“诀窍”是在HttpClient内部模拟HttpMessageHandler——而不是HttpClient本身。注意:使用MockBehavior是一个很好的实践。严格模拟,以便提醒您没有显式模拟和预期的任何调用。

RichardSzalay。MockHttp

RichardSzalay。MockHttp是另一个流行的解决方案。我以前使用过这个,但发现它比Moq.Contrib.HttpClient稍微麻烦一些。这里可以使用两种不同的模式。Richard在这里描述了什么时候使用其中一个和另一个。

Moq.Contrib.HttpClient

就像使用Moq本身的解决方案一样,如果您熟悉使用Moq框架,这是很简单的。我发现这个解决方案更直接,代码更少。这是我选择使用的解决方案。注意,这个解决方案需要一个独立于Moq本身的Nuget - Moq. contrib . httpclient

WireMock。网

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

它实际上是开放端口以方便测试。由于我过去不得不修复由于HttpClient使用不当而导致的端口耗尽问题,所以我决定放弃这个解决方案,因为我不确定它在并行运行许多单元测试的大型代码库中是否能很好地扩展。 使用的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; }
}

最小起订量(自行决定)

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

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

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

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

WireMock。网

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

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

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