我有一些问题,试图包装我的代码在单元测试中使用。问题在于。我有接口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的可扩展性在于传递给构造函数的HttpMessageHandler。它的目的是允许特定于平台的实现,但您也可以模拟它。不需要为HttpClient创建装饰器包装。

如果你更喜欢DSL而不是使用Moq,我在GitHub/Nuget上有一个库,它可以让事情变得更容易一些:https://github.com/richardszalay/mockhttp

Nuget包RichardSzalay。MockHttp可以在这里找到。

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 = 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'}

其他回答

受PointZeroTwo答案的启发,下面是一个使用NUnit和FakeItEasy的示例。

本例中的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(...);
    }
}

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

加入这个派对有点晚了,但我喜欢在带有下游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;
    }
}

and

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

一种替代方法是设置一个存根HTTP服务器,它根据与请求url匹配的模式返回罐装响应,这意味着您要测试真正的HTTP请求,而不是模拟。从历史上看,这将花费大量的开发工作,并将远远慢到考虑进行单元测试,然而,OSS库WireMock.net易于使用,足够快,可以运行大量的测试,所以可能值得考虑。Setup是几行代码:

var server = FluentMockServer.Start();
server.Given(
      Request.Create()
      .WithPath("/some/thing").UsingGet()
   )
   .RespondWith(
       Response.Create()
       .WithStatusCode(200)
       .WithHeader("Content-Type", "application/json")
       .WithBody("{'attr':'value'}")
   );

您可以在这里找到在测试中使用wiremock的更多细节和指导。

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

如果你更喜欢DSL而不是使用Moq,我在GitHub/Nuget上有一个库,它可以让事情变得更容易一些:https://github.com/richardszalay/mockhttp

Nuget包RichardSzalay。MockHttp可以在这里找到。

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 = 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'}