如何在 .NET 测试中传入模拟的 HttpClient?

Posted

技术标签:

【中文标题】如何在 .NET 测试中传入模拟的 HttpClient?【英文标题】:How to pass in a mocked HttpClient in a .NET test? 【发布时间】:2014-03-06 11:32:08 【问题描述】:

我有一个服务使用Microsoft.Net.Http 来检索一些Json 数据。太好了!

当然,我不希望我的单元测试到达实际的服务器(否则,这是一个集成测试)。

这是我的服务 ctor(使用依赖注入...)

public Foo(string name, HttpClient httpClient = null)

...

我不知道如何用 ... 说 .. MoqFakeItEasy 来模拟这个。

我想确保当我的服务调用 GetAsyncPostAsync 时,我可以伪造这些调用。

有什么建议吗?

我 - 希望 - 我不需要自己制作 Wrapper .. 因为那是废话 :( 微软不可能对此有所疏忽,对吧?

(是的,制作包装很容易......我以前做过......但这就是重点!)

【问题讨论】:

“我 -希望 - 我不需要自己制作 Wrapper” - 你使用了多少 HttpClient 的方法和属性?为这些创建一个接口通常会很有用,然后您可以对其进行模拟。 (就像我在 OP 中提到的那样)同意 - 这很容易和简单......但我会认为我不应该这样做?这应该是核心..对吗?这是我唯一的选择吗? 据我所知,如果这些呼叫是虚拟的,您可以将它们存根。因为他们不是,我假设你需要编写一个包装器(这是 imo 更清洁的方式)。 在这种情况下,请参阅Non Interface dependent Mocking Frameworks for C#,我通过 ".net mock not virtual methods without interface" 找到了它。接受的答案建议使用.NET Moles,这是我所知道的唯一一个无需虚拟方法即可工作的.NET 模拟框架。 【参考方案1】:

您可以将核心 HttpMessageHandler 替换为假的。看起来像这样的东西......

public class FakeResponseHandler : DelegatingHandler

    private readonly Dictionary<Uri, HttpResponseMessage> _FakeResponses = new Dictionary<Uri, HttpResponseMessage>();

    public void AddFakeResponse(Uri uri, HttpResponseMessage responseMessage)
    
        _FakeResponses.Add(uri, responseMessage);
    

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    
        if (_FakeResponses.ContainsKey(request.RequestUri))
        
            return Task.FromResult(_FakeResponses[request.RequestUri]);
        
        else
        
            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)  RequestMessage = request );
        
    

然后您可以创建一个使用假处理程序的客户端。

var fakeResponseHandler = new FakeResponseHandler();
fakeResponseHandler.AddFakeResponse(new Uri("http://example.org/test"), new HttpResponseMessage(HttpStatusCode.OK));

var httpClient = new HttpClient(fakeResponseHandler);

var response1 = await httpClient.GetAsync("http://example.org/notthere");
var response2 = await httpClient.GetAsync("http://example.org/test");

Assert.Equal(response1.StatusCode,HttpStatusCode.NotFound);
Assert.Equal(response2.StatusCode, HttpStatusCode.OK);

【讨论】:

很好的解决了一个有趣的问题。从字面上看,除了通过网络发送 HTTP 之外,它什么都做。 您的示例不会按原样编译。您需要使用 Task.FromResult 来返回您的 HttpResponseMessage 实例。 @Ananke SendAsync 方法中的 async 关键字为您带来了魔力。 有没有其他人认为这比使用IHttpClient 模拟接口复杂得多? 结果是对 DelegatingHandler.SendAsync 的 POST 调用,尽管方法名称为:***.com/questions/29106664/…【参考方案2】:

我知道这是一个老问题,但我在搜索该主题时偶然发现了这个问题,并找到了一个非常好的解决方案,可以让测试 HttpClient 更容易。

可以通过 nuget 获得:

https://github.com/richardszalay/mockhttp

PM> Install-Package 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://localost/api/user/1234");
// or without await: var response = client.GetAsync("http://localost/api/user/1234").Result;

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

// No network connection required
Console.Write(json); // 'name' : 'Test McGee'

github 项目页面上的更多信息。 希望这可以有用。

【讨论】:

【参考方案3】:

我只想对@Darrel Miller 的回答做一个小改动,它使用 Task.FromResult 来避免出现关于期望等待操作符的异步方法的警告。

public class FakeResponseHandler : DelegatingHandler

    private readonly Dictionary<Uri, HttpResponseMessage> _FakeResponses = new Dictionary<Uri, HttpResponseMessage>();

    public void AddFakeResponse(Uri uri, HttpResponseMessage responseMessage)
    
        _FakeResponses.Add(uri, responseMessage);
    

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    
        if (_FakeResponses.ContainsKey(request.RequestUri))
        
            return Task.FromResult(_FakeResponses[request.RequestUri]);
        
        else
        
            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)  RequestMessage = request );
        
    

【讨论】:

我会在任何对性能敏感的环境中谨慎使用该技术,因为它可能会导致线程切换而无益。如果您真的担心警告,那么只需删除 async 关键字并使用 Task.FromResult。 如果实现不是基于任务的,最好返回 Task.FromResult 并删除 async 关键字,而不是使用 Task.Run。 return Task.FromResult(response);【参考方案4】:

您可以查看Microsoft Fakes,尤其是Shims-部分。使用它们,您可以修改 HttpClient 本身的行为。先决条件是,您使用的是 VS Premium 或 Ultimate。

【讨论】:

我无法访问那两个昂贵的版本。

以上是关于如何在 .NET 测试中传入模拟的 HttpClient?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 .NET Core 中模拟/存根 HttpRequestMessage?

如何模拟 IOptionsSnapshot 实例进行测试

如何使用 Moq 在 .NET Core 2.1 中模拟新的 HttpClientFactory

.NET Core 单元测试 - 模拟 IOptions<T>

如何在 .NET 中的线程上传播 tcplistener 传入连接?

将模拟类注入方法以单元测试方法