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

56

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

当然,我不想让我的单元测试访问实际的服务器(否则,那就是集成测试)。

这是我的服务构造函数(使用依赖注入...)

public Foo(string name, HttpClient httpClient = null)
{
...
}

我不确定如何使用...比如Moq或者FakeItEasy来模拟这个。

我想确保当我的服务调用GetAsync或者PostAsync时,我可以伪造这些调用。

有什么建议吗?

我希望我不需要自己制作包装器..因为那很糟糕 :( Microsoft不可能会忽略这一点,对吧?

(是的,制作包装器很容易..我以前做过...但这是个原则问题!)


1
“我希望不需要自己创建包装器” - 你使用了HttpClient的多少个方法和属性?通常为这些创建一个接口可以证明很有用,然后你可以进行模拟。 - CodeCaster
(就像我在原帖中提到的那样)同意-这很容易和简单...但我认为我不应该这样做?这是核心内容...对吧?这是我的唯一选择吗? - Pure.Krome
1
据我所知,如果这些调用是虚拟的,你可以进行存根。但由于它们不是虚拟的,我认为你需要编写一个包装器(在我看来这是更清晰的方式)。 - ElGauchooo
在这种情况下,请参阅非接口依赖的C#模拟框架,我是通过“没有接口的虚拟方法的.net模拟”找到的。被采纳的答案建议使用.NET Moles,这是我所知道的唯一一个可以在没有虚拟方法的情况下工作的.NET模拟框架。 - CodeCaster
4个回答

96

你可以用一个虚假的核心HttpMessageHandler来替换原有的Handler。 代码类似于下面这样...

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

1
有趣问题的好解决方案。它几乎可以做任何事情,除了通过网络发送HTTP请求。 - Spence
1
你的示例无法编译。你需要使用Task.FromResult来返回你的HttpResponseMessage实例。 - Ananke
1
@Ananke SendAsync方法上的async关键字为您完成了这个魔法。 - Darrel Miller
1
还有其他人认为这比拥有一个IHttpClient接口进行模拟更加不必要地复杂吗? - Jeremy Holovacs
1
结果显示,在调用DelegatingHandler.SendAsync时,POST也经过了该方法: https://dev59.com/Vojca4cB1Zd3GeqP2dQO - jgerman
显示剩余4条评论

19

我知道这是一个老问题,但在搜索这个主题时我偶然发现了一个非常好的解决方案,可以使测试 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 项目页面。希望对您有所帮助。


2

我想对@Darrel Miller的回答进行小改动,使用Task.FromResult来避免警告,该警告提示需要使用await操作符来异步执行方法。

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

4
如果性能敏感的环境中,我会谨慎使用这种技术,因为它可能会导致线程切换却没有任何好处。如果您真的担心这个警告,那么只需删除async关键字并使用Task.FromResult即可。 - Darrel Miller
5
如果实现不是基于任务的,最好返回Task.FromResult并移除async关键字,而不是使用Task.Run。 返回Task.FromResult(response)。 - user3285954

1
你可以查看Microsoft Fakes,特别是其中的Shims部分。使用它们,你可以修改HttpClient本身的行为。前提条件是你使用的是VS Premium或Ultimate版本。

4
我无法获取那两个昂贵版本。 - Pure.Krome

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接