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


当前回答

我做了一些非常简单的事情,因为我在一个依赖注入环境中。

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

其他回答

您的接口公开了具体的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有一个可以模拟的接口,事情会简单得多,不幸的是它没有。

This example is not the golden ticket however. IHttpHandler still relies on HttpResponseMessage, which belongs to System.Net.Http namespace, therefore if you do need other implementations other than HttpClient, you will have to perform some kind of mapping to convert their responses into HttpResponseMessage objects. This of course is only a problem if you need to use multiple implementations of IHttpHandler but it doesn't look like you do so it's not the end of the world, but it's something to think about.

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

我建议测试非异步方法,因为这些方法仍然调用异步方法,但不必担心单元测试异步方法的麻烦,参见这里

正如注释中提到的,您需要抽象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相反的功能。使用抽象是一种干净(我的观点)和可靠的方法,并且可以使您的代码更易于维护,如果您需要在框架更改时将底层客户端切换为其他东西。

下面是如何实现的代码片段。

/// <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实例。如果您这样做,您将在运行时耗尽套接字(即使您正在处理HttpClient对象)。

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

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

    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。保护包。

我认为问题是你把它弄颠倒了。

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

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

如果你看了上面的类,我想这就是你想要的。微软建议保持客户端活跃以获得最佳性能,因此这种类型的结构允许您这样做。而且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上模拟受保护的方法。我随后添加了以下代码,以允许注入适当的mock。

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
}