Runtime: 模拟用于单元测试的 httpClient.GetAsync(..) 方法的简单方法?

创建于 2015-05-04  ·  157评论  ·  资料来源: dotnet/runtime

System.Net.Http现在已上传到仓库 :smile: :tada: :balloon:

每当我在某些服务中使用它时,它工作得很好,但很难进行单元测试 => 我的单元测试实际上并不想达到那个真正的终点。

很久以前,我问@davidfowl我们应该怎么做? 我希望我能解释一下,不要误引用他的话——但他建议我需要伪造一个消息处理程序(即HttpClientHandler ),把它连接起来,等等。

因此,我最终创建了一个名为HttpClient.Helpers的帮助程序库来帮助我运行单元测试。

所以这行得通......但感觉_非常_混乱和..复杂。 我确信我不是第一个需要确保我的单元测试不会真正调用外部服务的人。

有没有更简单的方法? 就像..我们不能只有一个IHttpClient接口,我们可以将它注入我们的服务吗?

Design Discussion area-System.Net.Http test enhancement

最有用的评论

@SidharthNabar - 感谢您阅读我的问题,我也非常感谢。

创建一个新的处理程序类

你刚刚回答了我的问题:)

这也是一个很大的舞步,只是为了让我的真实代码不要_命中网络_。

我什至制作了HttpClient.Helpers repo 和 nuget 包 .. 只是为了让测试更容易! 场景就像快乐路径或网络端点抛出的异常......

这就是问题所在 -> 我们可以不做所有这些并且......只是一个模拟方法吗?

我将尝试通过代码进行解释..

目标:从互联网上下载一些东西。

public async Foo GetSomethingFromTheInternetAsync()
{
    ....
    using (var httpClient = new HttpClient())
    {
        html = await httpClient.GetStringAsync("http://www.google.com.au");
    }
    ....
}

让我们看两个例子:

给定一个接口(如果存在):

public interface IHttpClient
{
    Task<string> GetStringAsync(string requestUri);    
}

我的代码现在看起来像这样......

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient()) // <-- CHANGE dotnet/corefx#1
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

和测试班

public async Task GivenAValidEndPoint_GetSomethingFromTheInternetAsync_ReturnsSomeData()
{
    // Create the Mock : FakeItEasy > Moq.
    var httpClient = A.Fake<IHttpClient>();

    // Define what the mock returns.
    A.CallTo(()=>httpClient.GetStringAsync("http://www.google.com.au")).Returns("some html here");

    // Inject the mock.
    var service = new SomeService(httpClient);
    ...
}

耶! 完毕。

好的,现在以当前的方式...

  1. 创建一个新的 Handler 类 - 是的,一个类!
  2. 将处理程序注入服务
public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null 
                                    ? new HttpClient()
                                    : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

但是现在痛苦的是我必须上课。 你现在伤害了我。

public class FakeHttpMessageHandler : HttpClientHandler
{
 ...
}

这门课一开始很简单。 直到我有多个GetAsync calls (所以我需要提供多个 Handler 实例?) - 或者 - 单个服务中有多个 httpClients。 此外,如果我们在同一个逻辑块中进行多次调用(这是一个 ctor 选项),我们是要Dispose处理程序还是重用它?

例如。

public async Foo GetSomethingFromTheInternetAsync()
{
    string[] results;
    using (var httpClient = new HttpClient())
    {
        var task1 = httpClient.GetStringAsync("http://www.google.com.au");
        var task2 = httpClient.GetStringAsync("http://www.microsoft.com.au");

        results = Task.WhenAll(task1, task2).Result;
    }
    ....
}

有了这个,这可以变得更容易..

var httpClient = A.Fake<IHttpClient>();
A.CallTo(() = >httpClient.GetStringAsync("http://www.google.com.au"))
    .Returns("gooz was here");
A.CallTo(() = >httpClient.GetStringAsync("http://www.microsoft.com.au"))
    .Returns("ms was here");

干净干净:)

然后 - 还有下一点:可发现

当我第一次开始使用MS.Net.Http.HttpClient时,api 非常明显:+1:好的 - 获取字符串并异步执行......简单......

...但是后来我进行了测试....现在我想了解一下HttpClientHandlers ? 嗯,为什么? 我觉得这一切都应该隐藏在幕后,我不必担心所有这些实现细节。 太多了! 你是说我应该开始看盒子里面的东西并学习一些管道......这很痛:cry:

国际海事组织,这一切都让事情变得比应有的复杂。


帮助我微软 - 你是我唯一的希望。

所有157条评论

HttpClient只不过是另一个 HTTP 库的抽象层。 它的主要目的之一是允许您替换行为(实现),包括但不限于创建确定性单元测试的能力。

在其他作品中, HttpClient本身既作为“真实”对象又作为“模拟”对象,而HttpMessageHandler是您选择以满足代码需求的对象。

但是,如果感觉设置消息处理程序需要做很多工作(感觉)这可以通过一个漂亮的界面来处理? 我完全误解了解决方案吗?

@PureKrome - 感谢您提出这个讨论。 您能否详细说明“设置消息处理程序需要做很多工作”的意思?

在不访问网络的情况下对 HttpClient 进行单元测试的一种方法是 -

  1. 创建一个派生自 HttpMessageHandler 的新 Handler 类(例如 FooHandler)
  2. 根据您的要求实施 SendAsync 方法 - 不要访问网络/记录请求-响应/等。
  3. 将此 FooHandler 的实例传递给 HttpClient 的构造函数:
    var handler = new FooHandler();
    var client = new HttpClient(handler);

然后,您的 HttpClient 对象将使用您的 Handler 而不是内置的 HttpClientHandler。

谢谢,
席德

@SidharthNabar - 感谢您阅读我的问题,我也非常感谢。

创建一个新的处理程序类

你刚刚回答了我的问题:)

这也是一个很大的舞步,只是为了让我的真实代码不要_命中网络_。

我什至制作了HttpClient.Helpers repo 和 nuget 包 .. 只是为了让测试更容易! 场景就像快乐路径或网络端点抛出的异常......

这就是问题所在 -> 我们可以不做所有这些并且......只是一个模拟方法吗?

我将尝试通过代码进行解释..

目标:从互联网上下载一些东西。

public async Foo GetSomethingFromTheInternetAsync()
{
    ....
    using (var httpClient = new HttpClient())
    {
        html = await httpClient.GetStringAsync("http://www.google.com.au");
    }
    ....
}

让我们看两个例子:

给定一个接口(如果存在):

public interface IHttpClient
{
    Task<string> GetStringAsync(string requestUri);    
}

我的代码现在看起来像这样......

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient()) // <-- CHANGE dotnet/corefx#1
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

和测试班

public async Task GivenAValidEndPoint_GetSomethingFromTheInternetAsync_ReturnsSomeData()
{
    // Create the Mock : FakeItEasy > Moq.
    var httpClient = A.Fake<IHttpClient>();

    // Define what the mock returns.
    A.CallTo(()=>httpClient.GetStringAsync("http://www.google.com.au")).Returns("some html here");

    // Inject the mock.
    var service = new SomeService(httpClient);
    ...
}

耶! 完毕。

好的,现在以当前的方式...

  1. 创建一个新的 Handler 类 - 是的,一个类!
  2. 将处理程序注入服务
public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null 
                                    ? new HttpClient()
                                    : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

但是现在痛苦的是我必须上课。 你现在伤害了我。

public class FakeHttpMessageHandler : HttpClientHandler
{
 ...
}

这门课一开始很简单。 直到我有多个GetAsync calls (所以我需要提供多个 Handler 实例?) - 或者 - 单个服务中有多个 httpClients。 此外,如果我们在同一个逻辑块中进行多次调用(这是一个 ctor 选项),我们是要Dispose处理程序还是重用它?

例如。

public async Foo GetSomethingFromTheInternetAsync()
{
    string[] results;
    using (var httpClient = new HttpClient())
    {
        var task1 = httpClient.GetStringAsync("http://www.google.com.au");
        var task2 = httpClient.GetStringAsync("http://www.microsoft.com.au");

        results = Task.WhenAll(task1, task2).Result;
    }
    ....
}

有了这个,这可以变得更容易..

var httpClient = A.Fake<IHttpClient>();
A.CallTo(() = >httpClient.GetStringAsync("http://www.google.com.au"))
    .Returns("gooz was here");
A.CallTo(() = >httpClient.GetStringAsync("http://www.microsoft.com.au"))
    .Returns("ms was here");

干净干净:)

然后 - 还有下一点:可发现

当我第一次开始使用MS.Net.Http.HttpClient时,api 非常明显:+1:好的 - 获取字符串并异步执行......简单......

...但是后来我进行了测试....现在我想了解一下HttpClientHandlers ? 嗯,为什么? 我觉得这一切都应该隐藏在幕后,我不必担心所有这些实现细节。 太多了! 你是说我应该开始看盒子里面的东西并学习一些管道......这很痛:cry:

国际海事组织,这一切都让事情变得比应有的复杂。


帮助我微软 - 你是我唯一的希望。

我也很想看到一种简单的方法来测试使用 HttpClient 的各种东西。 :+1:

感谢您提供详细的回复和代码片段 - 这真的很有帮助。

首先,我注意到您正在创建新的 HttpClient 实例来发送每个请求 - 这不是 HttpClient 的预期设计模式。 创建一个 HttpClient 实例并将其重用于所有请求有助于优化连接池和内存管理。 请考虑重用 HttpClient 的单个实例。 完成此操作后,您可以将假处理程序插入到 HttpClient 的一个实例中,然后就完成了。

第二 - 你是对的。 HttpClient 的 API 设计不适合纯粹的基于接口的测试机制。 我们正在考虑在未来添加一个静态方法/工厂模式,这将允许您更改从该工厂或在该方法之后创建的所有 HttpClient 实例的行为。 不过,我们还没有为此确定一个设计。 但关键问题仍然存在 - 您需要定义一个假 Handler 并将其插入 HttpClient 对象下方。

@ericstj - 对此有什么想法?

谢谢,
席德。

@SidharthNabar为什么您/团队如此犹豫是否要为该课程提供界面? 我希望开发人员必须 _learn_ 关于处理程序,然后必须 _create_ 伪造处理程序类的全部意义足以证明(或至少突出)对接口的需求?

是的,我不明白为什么接口会是一件坏事。 这将使测试 HttpClient 变得更加容易。

我们避免在框架中使用接口的主要原因之一是它们的版本不好。 人们总是可以在需要时创建自己的,并且在下一个版本的框架需要添加新成员时不会被破坏。 @KrzysztofCwalina@terrajobst可能能够分享更多此设计指南的历史/细节。 这个特殊的问题是一场近乎宗教的辩论:我太务实了,无法参与其中。

HttpClient 有很多用于单元可测试性的选项。 它不是密封的,它的大多数成员都是虚拟的。 正如@sharwell 指出的那样,它有一个可用于简化抽象的基类以及一个精心设计的HttpMessageHandler扩展点。 感谢@HenrikFrystykNielsen,IMO 是一个设计精良的 API。

:+1: 一个接口

:wave: @ericstj感谢大家加入 :+1: :cake:

我们避免在框架中使用接口的主要原因之一是它们的版本不好。

是的 - 好点。

这个特殊的问题是一个近乎宗教的辩论

是的……这点很好。

HttpClient 有很多用于单元可测试性的选项

哦? 我正在为此苦苦挣扎:脸红:因此出现此问题的原因:脸红:

它不是密封的,它的大多数成员都是虚拟的。

成员是虚拟的? 哦,drats,我完全错过了! 如果它们是虚拟的,那么模拟框架可以模拟这些成员:+1:我们不需要接口!

让我们看看 HttpClient.cs ...

public Task<HttpResponseMessage> GetAsync(string requestUri) { .. }
public Task<HttpResponseMessage> GetAsync(Uri requestUri) { .. }

唔。 那些不是虚拟的......让我们搜索文件......呃..没有找到virtual关键字。 那么你的意思是_其他_成员到_其他相关_类是虚拟的吗? 如果是这样..那么我们又回到了我提出的问题上——我们现在必须深入了解GetAsync正在做什么,这样我们就知道要创建什么/wireup/etc...

我想我只是不了解一些真正基本的东西,这里¯(°_°)/¯?

编辑:也许这些方法可以是虚拟的? 我可以公关!

SendAsync 是虚拟的,几乎所有其他 API 层都在此之上。 “大多数”是不正确的,我的记忆在那里错了。 我的印象是,大多数都是虚拟的,因为它们建立在虚拟成员之上。 通常,如果它们是级联重载,我们不会将它们虚拟化。 SendAsync 有一个更具体的重载,它不是虚拟的,可以修复。

啊! Gotcha :blush: 所以所有这些方法最终都会调用SendAsync .. 完成所有繁重的工作。 所以这仍然意味着我们在这里有一个可发现性问题......但让我们把它放在一边(这是固执己见)。

我们将如何模拟SendAsync给出这个基本示例......
Install-Package Microsoft.Net.Http
Install-Package xUnit

public class SomeService
{
    public async Task<string> GetSomeData(string url)
    {
        string result;
        using (var httpClient = new HttpClient())
        {
            result = await httpClient.GetStringAsync(url);
        }

        return result;
    }
}

public class Facts
{
    [Fact]
    public async Task GivenAValidUrl_GetSomeData_ReturnsSomeData()
    {
        // Arrange.
        var service = new SomeService();

        // Act.
        var result = await service.GetSomeData("http://www.google.com");

        // Assert.
        Assert.True(result.Length > 0);
    }
}

我们避免在框架中使用接口的主要原因之一是它们的版本不好。 人们总是可以在需要时创建自己的,并且在下一个版本的框架需要添加新成员时不会被破坏。

使用完整的 .NET Framework,我完全可以理解这种思维方式。

但是,.NET Core 被拆分为许多小包,每个包都独立进行版本控制。 使用语义版本控制,在发生重大更改时更新主要版本,然后就完成了。 唯一受影响的人将是那些明确将他们的包更新到新的主要版本的人——知道有记录的重大变化。

我并不是主张你应该每天都破坏每个接口:理想情况下,破坏性的更改应该一起批处理到一个新的主要版本中。

出于未来兼容性的原因,我觉得削弱 API 很可悲。 .NET Core 的目标之一不是加快迭代速度,因为您不必再​​担心细微的 .NET 更新会破坏数千个已安装的应用程序吗?

我的两分钱。

@MrJul :+1:
版本控制应该不是问题。 这就是版本号存在的原因:)

版本控制仍然是一个很大的问题。 向接口添加成员是一项重大更改。 对于像这样在桌面收件箱中的核心库,我们希望在未来版本中将功能带回桌面。 如果我们分叉这意味着人们不能编写将在两个地方运行的可移植代码。 有关重大更改的更多信息,请查看: https://github.com/dotnet/corefx/wiki/Breaking-Changes。

我认为这是一个很好的讨论,但正如我之前提到的,我在围绕接口与抽象类的争论中没有太多的划痕。 也许这是下一次设计会议的主题?

我对您正在使用的测试库不是很熟悉,但我要做的是提供一个钩子来允许测试设置客户端的实例,然后创建一个我想要的行为的模拟对象。 模拟对象可以是可变的以允许某些重用。

如果您想向 HttpClient 建议改进单元可测试性的特定兼容更改,请提出它, @SidharthNabar和他的团队可以考虑。

如果 API 不可模拟,我们应该修复它。 但这与接口与类无关。 出于所有实际目的,从可模拟性的角度来看,接口与纯抽象类没有什么不同。

@KrzysztofCwalina先生,你一针见血! 完美总结!

我想我会采取这个论点中不太受欢迎的一面,因为我个人认为没有必要使用接口。 正如已经指出的, HttpClient已经是抽象。 该行为确实来自HttpMessageHandler 。 是的,使用我们已经习惯使用 Moq 或 FakeItEasy 等框架的方法,它不是可模拟/可伪造的,但它不需要这样做; 这不是做事的唯一方式。 不过,API 仍然可以通过设计完美测试。

所以让我们来解决“我需要创建自己的HttpMessageHandler吗?”。 不,当然不。 我们并不是都编写了自己的模拟库。 @PureKrome已经展示了他的HttpClient.Helpers库。 我个人没有使用过那个,但我会检查一下。 我一直在使用@richardszalayMockHttp库,我觉得它很棒。 如果您曾经使用过 AngularJS 的 $httpBackend,那么 MockHttp 会采用完全相同的方法。

依赖关系方面,您的服务类应该允许注入HttpClient并且显然可以提供合理的默认值new HttpClient() 。 如果您需要能够创建实例,请使用Func<HttpClient> ,或者,如果您出于某种原因不喜欢Func<> ,请设计自己的IHttpClientFactory抽象和再次,默认实现将返回new HttpClient()

最后,我发现这个 API 以其当前形式完全可以测试,并且不需要接口。 是的,它需要一种不同的方法,但上述库已经存在,可以帮助我们以完全合乎逻辑、直观的方式进行这种不同的测试风格。 如果我们想要更多的功能/简单性,让我们为这些库做出贡献以使它们变得更好。

就个人而言,接口或虚拟方法对我来说并不重要。 但是来吧, perfectly testable有点多:)

@luisrudge好吧,您能否给出一个无法使用 MockHttp 之类的消息处理程序测试方式进行测试的场景? 也许这将有助于证明这一点。

我还没有遇到任何我无法编码的东西,但也许我错过了一些无法涵盖的更深奥的场景。 即便如此,也可能只是该库的一个缺失功能​​,有人可以贡献。

目前,我认为它作为依赖项是“完全可测试的”,只是不像 .NET 开发人员习惯的那样。

这是可测试的,我同意。 但这仍然是一种痛苦。 如果您必须使用可以帮助您做到这一点的外部库,那么它就不是完全可测试的。 但我想这太主观了,无法对此进行讨论。

有人可能会争辩说,aspnet mvc 5 是完全可测试的,你只需要编写 50LOC 来模拟控制器需要的每一件事。 恕我直言,这与 HttpClient 的情况相同。 可以测试吗? 是的。 容易测试吗? 不。

@luisrudge是的,我同意,这是主观的,我完全理解对界面/虚拟的渴望。 我只是想确保任何出现并阅读此线程的人至少会得到一些关于这个 API _可以_以非常可测试的方式在代码库中利用的事实的教育,而无需在HttpClient周围引入您自己的所有抽象

如果您必须使用可以帮助您做到这一点的外部库,则它不是完全可测试的

好吧,我们都已经在使用一个或另一个库来模拟/伪造*而且,确实,我们不能只使用 _that_ 一个来测试这种风格的 API,但我不认为这意味着它的可测试性比一种基于接口的方法,只是因为我必须引入另一个库。

_ *至少我希望不会!_

我的 OP 中的@drub0y我已经说过该库是可测试的 - 但与(与我热情地相信的)相比,这只是一个真正的痛苦。 IMO @luisrudge完美地说明了这一点:

有人可能会争辩说,aspnet mvc 5 是完全可测试的,你只需要编写 50LOC 来模拟控制器需要的每一件事

这个 repo 是大量开发人员的主要部分。 因此,默认的(并且可以理解的)策略是对这个 repo 非常谨慎。 当然——我完全明白。

我想相信这个 repo 在它进入 RTW 之前仍然可以调整(关于 API)。 未来的改变突然变得_really_很难做,包括_perception_要改变。

那么使用当前的 api - 可以测试吗? 是的。 它是否通过了暗物质开发者测试? 我个人不这么认为。

IMO 的试金石是这样的:一个普通的 joe 可以选择一个常见/流行的测试框架 + 模拟框架并模拟这个 API 中的任何主要方法吗? 现在——不。 因此,开发人员需要停止他们正在做的事情并开始了解这个库的_implimentation_。

正如已经指出的,HttpClient 已经是抽象。 该行为确实来自 HttpMessageHandler。

这就是我的观点 - 当图书馆的目的是抽象所有魔法时,为什么你让我们不得不花费周期来解决这个问题......只是说“好的......所以我们已经做了一些魔法,但现在你想模拟我们的魔法……对不起,你现在必须挽起袖子,掀开图书馆的屋顶,开始在里面挖,等等”。

感觉……好复杂。

我们能够让这么多人的生活变得轻松,并继续回到防御立场:“可以做到 - 但是..阅读常见问题解答/代码”。

这是解决此问题的另一个角度:给随机的非 MS 开发人员 2 个示例......知道如何使用 xUnit 和 Moq/FakeItEasy/InsertTheHipMockingFrameworkThisYear 的公民开发人员。

  • 第一个使用接口/虚拟/其他的 API .. 但可以模拟该方法。
  • 第二个 API 只是一个公共方法并模拟 _integration point_ .. 他们必须阅读代码。

它归结为,IMO。

看看哪个开发人员可以解决这个问题_并且_保持快乐。

如果 API 不可模拟,我们应该修复它。

现在它不是 IMO - 但有办法成功解决这个问题(再次,这是固执己见 - 我会承认这一点)

但这与接口与类无关。 出于所有实际目的,从可模拟性的角度来看,接口与纯抽象类没有什么不同。

完全正确 - 这是一个实现细节。 目标是能够使用经过实战考验的模拟框架,然后离开。

@luisrudge如果您必须使用可以帮助您做到这一点的外部库,那么它不是完全可测试的
@drub0y好吧,我们都在使用一个或另一个库来模拟/伪造

(我希望我理解最后的引用/段落..)不......安静。 @luisrudge的意思是:“我们有一个用于测试的工具。第二个用于模拟的工具。到目前为止,那些通用/整体工具与任何东西都没有联系。但是..你现在要我下载第三个工具,它是_specific_在我们的代码中模拟一个特定的 API/服务,因为我们正在使用的特定 API/服务不是为了很好地测试而设计的?”。 这有点丰富:(

依赖关系,您的服务类应该允许注入 HttpClient 并且显然可以提供合理的默认值 new HttpClient()

完全同意! 那么,你能重构上面的示例代码来向我们展示这应该/可以多么简单吗? 给猫剥皮的方法有很多……那么简单易行的方法是什么? 向我们展示代码。

我会开始的。 感谢@KrzysztofCwalina使用界面,但这只是为了启动我的试金石。

public interface IHttpClient
{
    Task<string> GetStringAsync(string url);
}

public class SomeService
{
    private IHttpClient _httpClient;

    // Injected.
    public SomeService(IHttpClient httpClient) { .. }

    public async Task<string> GetSomeData(string url)
    {
        string result;
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            result = await httpClient.GetStringAsync(url);
        }

        return result;
    }    
}

听起来@PureKrome需要的只是一个文档更新,解释了要模拟/覆盖哪些方法,以便在测试期间自定义 API 的行为。

@sharwell绝对不是这样。 当我测试东西时,我不想运行整个 httpclient 代码:
查看SendAsync 方法
这太疯狂了。 您的建议是每个开发人员都应该了解课程的内部结构,打开 github,查看代码和能够测试它的那种东西。 我希望该开发人员模拟GetStringAsync并完成工作。

@luisrudge实际上,我的建议是完全避免嘲笑这个库。 由于您已经有了一个带有解耦、可替换实现的干净抽象层,因此不需要额外的模拟。 模拟这个库有以下额外的缺点:

  1. 增加了开发人员创建测试(维护模拟)的负担,这反过来又增加了开发成本
  2. 您的测试无法检测到某些有问题的行为的可能性增加,例如重用与消息关联的内容流(发送消息会导致在内容流上调用Dispose ,但大多数模拟会忽略这一点)
  3. 应用程序与特定抽象层不必要地紧密耦合,这增加了与评估HttpClient替代方案相关的开发成本

Mocking 是一种针对难以小规模测试的相互交织的代码库的测试策略。 虽然使用模拟与增加的开发成本相关,但模拟的_need_与代码中耦合量的增加相关。 这意味着模拟本身就是一种代码味道。 如果您可以在不使用模拟的情况下跨 API 和在 API 内提供相同的输入输出测试覆盖率,那么您将在基本上每个开发方面受益。

由于您已经有了一个带有解耦、可替换实现的干净抽象层,因此不需要额外的模拟

恕我直言,如果选择必须编写自己的伪造消息处理程序,这在不深入研究代码的情况下并不明显,那么这是一个非常高的障碍,会让许多开发人员感到非常沮丧。

我同意@luisrudge之前的评论。 _技术上_ MVC5 是可测试的,但我的天哪,它是一个让人头疼的问题,而且是一个巨大的拉扯头发的来源。

@sharwell你是说嘲笑IHttpClient是一种负担并实现一个假消息处理程序(就像这个不是?我不能同意,对不起。

@luisrudge对于测试GetStringAsync的简单案例,模拟处理程序并不难。 使用开箱即用的 Moq + HttpClient,您所需要的只是:

Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";

Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) }));
HttpClient httpClient = new HttpClient(mockHandler.Object);
string result = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false);
Assert.AreEqual(expectedResponse, result);

如果这太多了,您可以定义一个辅助类:

internal static class MockHttpClientHandlerExtensions
{
    public static void SetupGetStringAsync(this Mock<HttpClientHandler> mockHandler, Uri requestUri, string response)
    {
        mockHandler.Protected()
            .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
            .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(response) }));
    }
}

使用助手类,您只需要这样:

Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";

Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.SetupGetStringAsync(requestUri, expectedResponse);
HttpClient httpClient = new HttpClient(mockHandler.Object);
string result = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false);
Assert.AreEqual(expectedResponse, result);

所以比较两个版本:
_Disclaimer_:这是未经测试的浏览器代码。 另外,我已经很久没有使用起订量了。

当前的

public class SomeService(HttpClientHandler httpClientHandler= null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null
                                  ? new HttpClient
                                  : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

// Unit test...
// Arrange.
Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) };
Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(responseMessage);
HttpClient httpClient = new HttpClient(mockHandler.Object);
var someService = new SomeService(mockHandler.Object); // Injected...

// Act.
string result = await someService.GetSomethingFromTheInternetAsync();

// Assert.
Assert.AreEqual(expectedResponse, result);
httpClient.Verify(x => x.GetSomethingFromTheInternetAsync(), Times.Once);

而不是提供一种直接模拟 API 方法的方法。

另一种方式

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

// Unit tests...
// Arrange.
var response = new Foo();
var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.GetStringAsync(It.IsAny<string>))
    .ReturnsAsync(response);
var service = new SomeService(httpClient.Object); // Injected.

// Act.
var result = await service.GetSomethingFromTheInternetAsync();

// Assert.
Assert.Equals("something", foo.Something);
httpClient.Verify(x => x.GetStringAsync(It.IsAny<string>()), Times.Once);

提炼下来,主要是这个(不包括SomeService中的细微差别)......

var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) };
Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(responseMessage);
HttpClient httpClient = new HttpClient(mockHandler.Object);

与这个...

var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.GetStringAsync(It.IsAny<string>))
    .ReturnsAsync(response);

代码_roughly_看起来对两者都合适吗? (在我们比较两者之前,两者都必须相当准确)。

@sharwell所以我必须通过所有 HttpClient.cs 代码运行我的代码才能运行我的测试? 与 HttpClient 的耦合度如何降低?

我们在某些系统中大量使用 HttpClient,我不会称它为完美的可测试技术。 我真的很喜欢这个库,但是拥有某种接口或任何其他测试改进将是 IMO 的一项重大改进。

Kudo 为他的图书馆向@PureKrome 致敬,但如果没有必要的话会更好:)

我刚刚阅读了所有评论并学到了很多东西,但是我有一个问题:为什么需要在测试中模拟 HttpClient ?

在最常见的场景中,由HttpClient提供的功能已经被包装在某个类中,例如HtmlDownloader.DownloadFile() 。 为什么不只是模拟这个类而不是它的管道?

另一种可能需要这种模拟的情况是,当您必须解析下载的字符串并且这段代码需要测试多个不同的输出时。 即使在这种情况下,这个功能(解析)也可以被提取到一个类/方法中并在没有任何模拟的情况下进行测试。

我并不是说它的可测试性不能提高,也不是暗示不应该。 我只是想了解学习一些新东西。

@tucaz假设我有一个网站,可以返回一些关于你自己的信息。 你的年龄、姓名、生日等。你也碰巧持有一些公司的股票。

您说过您拥有 100 股 AAA 股和 200 股 BBB 股吗? 因此,当我们显示您的帐户详细信息时,我们_应该_显示您的股票值多少钱。

为此,我们需要从_another_ 系统中获取当前股票价格。 所以要做到这一点,我们需要使用服务来检索股票,对吧? 让这...

_免责声明:浏览器代码..._

public class StockService : IStockService
{
    private readonly IHttpClient _httpClient;

    public StockService(IHttpClient httpClient)
    {
        _httpClient = httpClient; // Optional.
    }

    public async Task<IEnumerable<StockResults>> GetStocksAsync(IEnumerable<string> stockCodes)
    {
        var queryString = ConvertStockCodeListIntoQuerystring();
        string jsonResult;
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            jsonResult = await httpClient.GetStringAsync("http:\\www.stocksOnline.com\stockResults?" + queryString);
        }

        return JsonConvert.DeserializeObject<StockResult>(jsonResult);
    }
}

好的,所以这里我们有一个简单的服务,它说:“_Go get me the stock price for stock AAA and BBB_”;

所以,我需要测试一下:

  • 给定一些合法的股票名称
  • 给定一些无效/不存在的股票名称
  • httpClient 抛出异常(等待响应时互联网爆炸)

现在我可以轻松地测试这些场景。 我注意到当 httpClient 爆炸(抛出异常)时..这个服务爆炸了..所以也许我需要添加一些日志记录并返回 null? donno ..但至少我可以考虑问题然后处理它。

当然——我 100% 不想在我的正常单元测试期间真正使用真正的 Web 服务。 通过注入模拟,我可以使用它。 否则我可以创建一个新实例并使用它。

所以我试图确定我的建议比当前的现状更容易(阅读/维护/等)。

在最常见的场景中,HttpClient 提供的功能已经被包装在一些类中,例如 HtmlDownloader.DownloadFile()。

为什么要包装 HttpClient? 它_已经_是对下面所有 http :sparkles: 的抽象。 这是一个很棒的图书馆! 我个人看不出这会达到什么目的?

@PureKrome您的使用示例正是我将功能包装在包装类中的意思。

我不确定我是否能理解您要测试的内容,但对我而言,您似乎正在测试底层 Web 服务,而您真正需要断言的是,无论谁在使用它,都将能够处理从GetStockAsync中返回的任何内容

请考虑以下几点:

private IStockService stockService;

[Setup]
public void Setup()
{
    stockService = A.Fake<IStockService>();

    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker1" }))
        .Returns(new StockResults[] { new StockResults { Price = 100m, Ticker = "Ticker1", Status = "OK" }});
    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker2" }))
        .Returns(new StockResults[] { new StockResults { Price = 0m, Ticker = "Ticker2", Status = "NotFound" }});
    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker3" }))
        .Throws(() => new InvalidOperationException("Some weird message"));
}

[Test]
public async void Get_Total_Stock_Quotes()
{
    var stockService = A.Fake<IStockService>();

    var total = await stockService.GetStockAsync(new [] { "Ticker1" });

    Assert.IsNotNull(total);
    Assert.IsGreaterThan(0, total.Sum(x => x.Price);
}

[Test]
public async void Hints_When_Ticker_Not_Found()
{
    var stockService = A.Fake<IStockService>();

    var total = await stockService.GetStockAsync(new [] { "Ticker2" });

    Assert.IsNotNull(total);
    Assert.AnyIs(x => x.Status == "NotFound");
}

[Test]
public async void Throws_InvalidOperationException_When_Error()
{
    var stockService = A.Fake<IStockService>();

    Assert.Throws(() => await stockService.GetStockAsync(new [] { "Ticker3" }), typeof(InvalidOperationException));
}

我建议您将抽象提高一级并处理实际的业务逻辑,因为没有任何需要测试GetStockAsync方法的行为的东西。

我能想到的唯一需要模拟HttpClient的场景是当您需要根据从服务器收到的响应或类似的东西来测试一些重试逻辑时。 在这种情况下,您将构建一段更复杂的代码,它可能会被提取到HttpClientWithRetryLogic类中,并且不会散布在您编写的每个使用HttpClient类的方法中。

如果是这种情况,您可以按照建议使用消息处理程序或构建一个类来包装所有这些重试逻辑,因为HttpClient将是您的类所做的重要事情的一小部分。

@tucaz您不是只是测试了您的假货是否返回了您配置的内容,从而有效地测试了模拟库吗? @PureKrome想要测试具体的StockService实现。

新引入的抽象级别应该包装HttpClient调用,以便能够正确测试StockService 。 包装器可能会简单地将每次调用委托给HttpClient ,并为应用程序增加不必要的复杂性。

您总是可以添加一个新的抽象层来隐藏不可模拟的类型。 在这个线程中批评的是,模拟HttpClient是可能的,但应该更容易/与其他库通常所做的一致。

@MrJul你是对的。 这就是为什么我说他的示例测试的是 web 服务实现本身以及为什么我建议(没有代码示例。我的错)他嘲笑IStockService以便他可以断言使用它的代码能够处理GetStockAsync返回的任何东西。

这就是我认为应该首先测试的内容。 不是模拟框架(就像我所做的那样)或 web 服务实现(就像@PureKrome想要做的那样)。

归根结底,我可以理解拥有一个IHttpClient接口会很好,但是在这些场景中看不到任何实用程序,因为 IMO 需要测试的是服务抽象的消费者如何(因此 web 服务)可以适当地处理它的返回。

@tucaz不。 你的测试是错误的。 您正在测试模拟库。 使用@PureKrome的示例,人们想要测试三件事:

  • 如果 http 请求使用正确的METHOD
  • 如果http请求有正确的URL
  • 如果响应体可以使用 json 反序列化器成功反序列化

如果您只是在控制器中模拟IStockService (例如),那么您并没有测试上面提到的三件事。

@luisrudge同意你的所有观点。 但是……测试你列举的所有这些东西的价值值得吗? 我的意思是,如果我致力于测试多个框架(json.net、httpclient 等)涵盖的所有基本内容,我想我可以处理今天可能做到的方式。

尽管如此,如果我想确保我可以反序列化一些奇怪的响应,我不能只做一个测试来隔离代码的这些方面而不需要在测试中添加HttpClient吗? 此外,无法阻止服务器始终发送您在测试中期望的正确响应。 如果有未涵盖的响应怎么办? 你的测试不会得到它,无论如何你都会遇到问题。

我的观点是,您提议的此类测试具有最小的净价值,只有在您涵盖了其他 99% 的应用程序时,您才应该对其进行处理。 但归根结底,这完全是个人喜好问题,这是一个主观主题,不同的人看到不同的价值。 当然,这并不意味着HttpClient不应该更具可测试性。

我认为这段特定代码的设计考虑了所有这些点,即使它不是最佳结果,我也不认为现在更改它的成本会低于可能的收益。

当我第一次说话时,我的目标是帮助@PureKrome尽量不要将他的精力集中在我认为是低价值的任务上。 但同样,这一切都归结为个人意见。

确保您点击正确的网址对您来说净值低? 确保您执行 PUT 请求来编辑资源而不是帖子怎么样? 我的应用程序的其他 99% 可以被覆盖,如果这个调用失败,那么什么都不会起作用。

太棒了,我真的认为谈论HttpClient::GetStringAsync是一种糟糕的方式来证明能够首先模拟HttpClient 。 如果您正在使用任何HttpClient::Get[String|Stream|ByteArray]Async方法,那么您已经丢失了,因为您无法进行任何适当的错误处理,因为所有内容都隐藏在HttpRequestException后面,这将被抛出并且不会甚至不公开诸如 HTTP 状态代码之类的重要细节。 因此,这意味着您在HttpClient之上构建的任何抽象都无法对特定的 HTTP 错误做出反应并将其转换为特定于域的异常,而现在您已经有了一个泄漏的抽象......这为您的域服务的调用者提供了可怕的异常信息。

我认为指出这一点很重要,因为在大多数情况下,这意味着如果您使用模拟方法,您将模拟返回HttpResponseMessageHttpClient方法,这已经让您到达模拟/伪造HttpMessageHandler::SendAsync所需的工作量相同。 其实还有更多! 您可能必须在HttpClient级别模拟多个方法(例如[Get|Put|Delete|Send]Async ),而不是在HttpMessageHandler级别简单地模拟SendAsync $。

@luisrudge还提到了反序列化 JSON,所以现在我们正在讨论使用HttpContent这意味着您正在使用肯定返回HttpResponseMessage的方法之一。 除非您说您要绕过整个媒体格式化程序体系结构并使用JsonSerializer::Deserialize或其他方式手动将字符串从GetStringAsync反序列化为对象实例。 如果是这样的话,那么我认为我们不能再就_testing_ API 进行对话了,因为那样只会使用_itself_ 完全错误的API。 :失望的:

@luisrudge确实很重要,但是您无需触摸HttpClient即可确保它正确。 只需确保您从中读取 URL 的来源正确返回内容,您就可以开始了。 同样,不需要在其中使用HttpClient进行集成测试。

这些都是重要的事情,但是一旦你把它们弄对了,除非你在其他地方有一个泄漏的管道,否则搞砸的机会真的非常非常低。

@tucaz同意。 仍然:这是最重要的部分,我想对其进行测试:)

@drub0y我很惊讶使用公共方法是错误的 :)

@luisrudge好吧,我想这是一个不同的讨论,但我不认为这是一个可以通过忽略使用这些“方便”方法的固有缺陷来模拟的接口,除了最补救的编码场景(例如实用程序和舞台演示)。 如果您选择在生产代码中使用它们,那是您的选择,但您这样做的同时希望承认它们的缺点。

无论如何,让我们回到@PureKrome想要了解的内容,即应该如何设计他们的类以适应HttpClient API 设计,以便他们可以轻松地测试他们的代码。

首先,这是一个示例域服务,它将发出 HTTP 调用以满足其域的需求:

``` C#
公共接口 ISomeDomainService
{
任务GetSomeThingsValueAsync(字符串 id);
}

公共类 SomeDomainService : ISomeDomainService
{
私有只读 HttpClient _httpClient;

public SomeDomainService() : this(new HttpClient())
{

}

public SomeDomainService(HttpClient httpClient)
{
    _httpClient = httpClient;
}

public async Task<string> GetSomeThingsValueAsync(string id)
{
    return await _httpClient.GetStringAsync("/things/" + Uri.EscapeUriString(id));
}

}

As you can see, it allows an `HttpClient` instance to be injected and also has a default constructor that instantiates the default `HttpClient` with no other special settings (obviously this default instance can be customized, but I'll leave that out for brevity).

Ok, so what's a test method look like for this thing using the MockHttp library then? Let's see:

``` c#
[Fact]
public async Task GettingSomeThingsValueReturnsExpectedValue()
{
    // Arrange
    var mockHttpMessageHandler = new MockHttpMessageHandler();
    mockHttpMessageHandler.Expect("http://unittest/things/123")
        .Respond(new StringContent("expected value"));

    SomeDomainService sut = new SomeDomainService(new HttpClient(mockHttpMessageHandler)
    {
        BaseAddress = new Uri("http://unittest")
    });

    // Act
    var value = await sut.GetSomeThingsValueAsync("123");

    // Assert
    value.Should().Be("expected value");
}

呃,这很简单直接; 读起来很清楚,就像 IMNSHO 一样。 另外,请注意,我正在利用 FluentAssertions,这也是我认为我们很多人都在使用的另一个库,也是对“我不想引入另一个库”论点的又一次打击。

现在,让我也说明一下为什么您可能永远不会_真的_想将GetStringAsync用于任何生产代码。 回顾一下SomeDomainService::GetSomeThingsValueAsync方法,假设它正在调用一个实际返回有意义的 HTTP 状态代码的 REST API。 例如,id 为“123”的“事物”可能不存在,因此 API 将返回 404 状态。 所以我想检测到它并将其建模为以下特定于域的异常:

``` C#
// 为简洁起见,排除了序列化支持
公共类 SomeThingDoesntExistException :异常
{
公共 SomeThingDoesntExistException(字符串 id)
{
身份证=身份证;
}

public string Id
{
    get;
    private set;
}

}

Ok, so let's rewrite the `SomeDomainService::GetSomeThingsValueAsync` method to do that now:

``` c#
public async Task<string> GetSomeThingsValueAsync(string id)
{
    try
    {
        return await _httpClient.GetStringAsync("/things/" + Uri.EscapeUriString(id));
    }
    catch(HttpRequestException requestException)
    {
        // uh, oh... no way to tell if it doesn't exist (404) or server maybe just errored (500)
    }
}

所以当我说像GetStringAsync这样的基本“便利”方法对于生产级代码来说并不是真的“好”时,这就是我所说的。 你所能得到的只是HttpRequestException并且从那里你不能知道你得到了什么状态代码,因此不可能将它翻译成域内的正确异常。 相反,您需要使用GetAsync并像这样解释HttpResponseMessage

``` C#
公共异步任务GetSomeThingsValueAsync(字符串 id)
{
HttpResponseMessage responseMessage = await _httpClient.GetAsync("/things/" + Uri.EscapeUriString(id));

if(responseMessage.IsSuccessStatusCode)
{
    return await responseMessage.Content.ReadAsStringAsync();
}
else
{
    switch(responseMessage.StatusCode)
    {
        case HttpStatusCode.NotFound:
            throw new SomeThingDoesntExistException(id);

        // any other cases you want to might want to handle specifically for your domain

        default:
            // Unhandled cases can throw domain specific lower level communication exceptions
            throw new HttpCommunicationException(responseMessage.StatusCode, responseMessage.ReasonPhrase);
    }
}

}

Ok, so now let's write a test to validate that I get my domain specific exception when I request a URL that doesn't exist:

``` C#
[Fact]
public void GettingSomeThingsValueForIdThatDoesntExistThrowsExpectedException()
{
    // Arrange
    var mockHttpMessageHandler = new MockHttpMessageHandler();

    SomeDomainService sut = new SomeDomainService(new HttpClient(mockHttpMessageHandler)
    {
        BaseAddress = new Uri("http://unittest")
    });

    // Act
    Func<Task> action = async () => await sut.GetSomeThingsValueAsync("SomeIdThatDoesntExist");

    // Assert
    action.ShouldThrow<SomeThingDoesntExistException>();
}

Ermahgerd 它的代码更少!!$!% 为什么? 因为我什至不必使用 MockHttp 设置期望值,它会自动为我请求的 URL 返回 404 作为默认行为。

Magic is Real

@PureKrome还提到了他想从服务类中实例化新的HttpClient实例的情况。 就像我之前说的那样,在这种情况下,您可以将Func<HttpClient>作为依赖项,而不是将自己从实际创建中抽象出来,或者如果您不是Func的粉丝,您可以引入自己的工厂接口

我删除了我的回复 - 我想考虑一下@drub0y的回复和代码。

@drub0y你忘了提到有人必须弄清楚首先需要实现一个MockHttpMessageHandler 。 这就是本次讨论的重点。 这还不够直截了当。 使用 aspnet5,您可以使用虚拟方法从接口或基类中模拟公共类来测试所有内容,但是这个特定的 API 条目是不同的,不易被发现且不太直观。

@luisrudge当然,我同意这一点,但是我们怎么知道我们需要起订量或 FakeItEasy? 我们如何确定我们需要 N/XUnit、FluentAssertions 等? 我真的不认为这有什么不同。

社区中有很多优秀(聪明)的人认识到对这些东西的需求,并实际上花时间启动这样的库来解决这些问题。 (向他们所有人致敬!)然后,实际上“好”的解决方案会被其他认识到相同需求的人注意到,并通过 OSS 和社区中其他领导者的宣传开始有机地发展。 我认为这方面的情况比以往任何时候都好,因为 OSS 的力量最终在 .NET 社区中得以实现。

现在,就是说,我个人认为微软应该提供一些 _like_ MockHttp 以及“开箱即用”的System.Net.Http API 的交付。 AngularJS 团队提供了$httpBackEnd$http ,因为他们认识到人们绝对需要_这种能力才能有效地测试他们的应用程序。 我认为微软_没有_识别出同样的问题是为什么围绕这个 API 的最佳实践存在如此多的“困惑”,我想为什么我们现在都在这里讨论它。 :失望:话虽如此,我很高兴@richardszalay@PureKrome已经站出来尝试用他们的图书馆填补这一空白,现在我认为我们应该支持他们帮助改进他们的图书馆,但我们可以尽一切努力我们的生活更轻松。 :微笑:

刚收到这个帖子的通知,所以我想我会把我的 2c 扔进去。

我和其他人一样使用动态模拟框架,但近年来我开始意识到某些领域(如 HTTP)在具有更多领域知识的东西后面被伪造更好。 使用动态模拟,除了基本的 URL 匹配之外的任何事情都会非常痛苦,并且当您对其进行迭代时,您最终会得到您自己的 MockHttp 所做的迷你版本。

以 Rx 为例:IObservable 是一个接口,因此是可模拟的,但是仅使用动态模拟来测试复杂的组合(更不用说调度程序)是很疯狂的。 Rx 测试库为其工作的领域提供了更多的语义含义。

具体回到 HTTP,尽管在 JS 世界中,您绝对也可以使用 sinon 来模拟 XMLHttpRequest,而不是使用 Angular 的测试 API,但如果有人这样做,我会感到非常惊讶。

说了这么多,我完全同意可发现性。 Angular 的 HTTP 测试文档和它的 HTTP 文档就在那里,但是你需要了解 MockHttp 才能找到它。 因此,如果 CoreFx 团队中的任何一个想要联系(许可证已经相同),我完全可以将 MockHttp 贡献回主库。

我喜欢嘲笑它,但我们也使用Func签名:

public static async Task<T> GetWebObjectAsync<T>(Func<string, Task<string>> getHtmlAsync, string url)
{
    var html = await getHtmlAsync(url);
    var info = JsonConvert.DeserializeObject<T>(html);
    return info;
}

这允许 Test 类代码如此简单:

Func<string, Task<string>> getHtmlAsync = u => Task.FromResult(EXPECTED_HTML);
var result = controller.InternalCallTheWeb(getHtmlAsync);

控制器可以简单地做:

public static HttpClient InitHttpClient()
{
    return _httpClient ?? (_httpClient = new HttpClient(new WebRequestHandler
    {
        CachePolicy = new HttpRequestCachePolicy(HttpCacheAgeControl.MaxAge, new TimeSpan(0, 1, 0)),
        Credentials = new NetworkCredential(PublishingCredentialsUserName, PublishingCredentialsPassword)
    }, true));
}

[Route("Information")]
public async Task<IHttpActionResult> GetInformation()
{
    var httpClient = InitHttpClient();
    var result = await InternalCallTheWeb(async u => await httpClient.GetStringAsync(u));
    ...
}

但他们可以让它更容易一些...... :)

嗨 .NET 团队! 只是触及这个问题的基础。 它已经存在一个多月了,只是对团队对这次讨论的看法感到好奇。

阵营双方都有不同的观点-所有有趣且相关的IMO。

你们有什么更新吗?

IE。

  1. 我们读过convo,不会改变任何事情。 感谢您的意见,祝您有美好的一天。 在这里,吃一块:蛋糕:
  2. 老实说,我们还没有考虑过(正如您想象的那样,这里的事情很忙)_但是_会在以后_在我们去发布到网络(RTW)之前考虑一下。
  3. 实际上,我们已经对此进行了一些内部讨论,我们承认公众发现它有点痛苦。 所以我们在想我们可能会做 XXXXXXXXX。

塔!

这是第 2 部分和第 3 部分。

一种简化插入“模拟”处理程序以帮助测试的能力的方法是将静态方法添加到 HttpClient,即 HttpClient.DefaultHandler。 此静态方法将允许开发人员在调用默认 HttpClient() 构造函数时设置与当前 HttpClientHandler 不同的默认处理程序。 除了帮助针对“模拟”网络进行测试之外,它还将帮助开发人员将代码编写到 HttpClient 之上的可移植库中,他们不会直接创建 HttpClient 实例。 因此,它将允许他们“注入”这个备用默认处理程序。

因此,这是我们可以添加到当前对象模型中的一个示例,它可以是附加的,而不是对当前架构的重大更改。

感谢@davidsh的及时回复:+1:

我会很高兴地等待并观看这个空间,因为知道它不是第 1 部分是令人兴奋的 :smile: .. 并期待看到发生的任何变化 :+1:

再次感谢(团队)的耐心和倾听——真的非常感谢!

/我愉快地等待着。

@davidsh那么,接口操作系统虚拟方法是没有问题的?

这不是问题。 但是本期主题中建议的短期添加接口需要仔细研究和设计。 它不容易映射到 HttpClient 的现有对象模型,并且可能是对 API 表面的重大更改。 因此,它不是快速或容易设计/实现的东西。

虚方法呢?

是的,添加虚拟方法不是一个突破性的变化,而是一个附加的变化。 我们仍在研究该领域可行的设计更改。

/me 在此线程上投射Resurrection ...

〜线程扭曲,呻吟,抽搐并活跃起来!


刚刚在听2015 年 8 月 20 日的 Community StandUp ,他们提到 HttpClient 将与 Beta7 跨平台使用。 耶!

那么……有没有对此作出任何决定? 不建议任何一个/或..只是礼貌地询问任何讨论更新等?

@PureKrome对此没有任何决定,但这项 API 设计/调查工作已纳入我们未来的计划。 现在,我们大多数人都非常专注于将 System.Net 的其余代码放到 GitHub 上并跨平台工作。 这包括现有的源代码和将我们的测试移植到 Xunit 框架。 一旦这项工作集中起来,团队将有更多时间开始关注未来的变化,就像这个线程中建议的那样。

谢谢@davidsh - 我非常感谢您抽出宝贵时间回复:) 继续出色的工作! :气球:

使用 mocks 来测试 http 服务是不是有点过时了? 有很多自托管选项可以轻松模拟 http 服务,并且不需要更改代码,只需更改服务 url。 即 HttpListener 和 OWIN 自托管,但对于复杂的 API,甚至可以构建模拟服务或使用自动回复。

image

image

这个 HttpClient 类的单元测试能力有什么更新吗? 我正在尝试模拟 DeleteAsync 方法,但如前所述,Moq 抱怨功能不是虚拟的。 似乎没有比创建我们自己的包装器更简单的方法了。

我投票给 Richardszalay.Mockhttp!
太棒了! 简单而精彩!

https://github.com/richardszalay/mockhttp

但是@YehudahA - 我们不应该需要那个......(这就是这个车队的重点):)

@PureKrome我们不应该需要IHttpClient
这与 EF 7 的想法相同。您从不使用 IDbContext 或 IDbSet,但您只需使用 DbContextOptions 更改行为。

Richardszalay.MockHttp 是一个开箱即用的解决方案。

这是一个开箱即用的解决方案。

我们不应该需要 IHttpClient。
这与 EF 7 的想法相同。您从不使用 IDbContext 或 IDbSet,但您只需使用 DbContextOptions 更改行为。

没关系 - 我完全不同意,所以我们同意不同意。

两种方式都有效吗? 是的。 出于可发现性、可读性和简单性等原因,有些人(比如我自己)发现模拟接口或虚拟方法更容易。

其他人(比如你自己)喜欢操纵这种行为。 我发现这更复杂,更难发现并且更耗时。 ((对我而言)意味着更难支持/维护)。

每个人都有自己的,我猜:)

Richardszalay 说:“这种模式很大程度上受到 AngularJS 的 $httpBackend 的启发”。
这是正确的。 让我们在这里看看: https ://docs.angularjs.org/api/ngMock/service/ $httpBackend

@PureKrome
是的,它更简单。
您不必知道使用了哪些类和方法,您不需要模拟库/接口/虚拟方法,如果您从 HttpClient 切换到其他东西,设置不会改变。

        private static IDisposable FakeService(string uri, string response)
        {
            var httpListener = new HttpListener();
            httpListener.Prefixes.Add(uri);

            httpListener.Start();

            httpListener.GetContextAsync().ContinueWith(task =>
            {
                var context = task.Result;

                var buffer = Encoding.UTF8.GetBytes(response);

                context.Response.OutputStream.Write(buffer, 0, buffer.Length);

                context.Response.OutputStream.Close();
            });

            return httpListener;
        }

用法:

            var uri = "http://localhost:8888/myservice/";
            var fakeResponse = "Hello World";

            using (FakeService(uri, fakeResponse))
            {
                // Run your test here ...

                // This is only to test that is really working:
                using (var client = new HttpClient())
                {
                    var result = client.GetStringAsync(uri).Result;
                }
            }

@metzgithub是这里最好的一个,因为它允许正确模拟目标服务的行为。

这正是我一直在寻找的,因为它为我提供了放置“格式错误/恶意”数据的完美位置(用于安全性和非快乐路径测试)

我来晚了,我在搜索了人们如何单元测试他们对 HttpClient 的依赖后到达这里,并找到了一些非常有创意的方法。 我会预先声明,在这个论点中,我会坚定地站在界面一边。 然而,对我来说,问题是我别无选择。

我选择设计和开发声明它们依赖于其他组件的组件和服务。 我强烈倾向于选择通过构造函数声明这些依赖项,并在运行时依赖 IoC 容器来注入具体实现并在此时控制它是单例还是瞬态实例。

我选择使用我在大约十年前学到的可测试性的定义,同时拥抱当时相对较新的测试驱动开发实践:

“一个类是可测试的,如果它可以从它的正常环境中删除并且在它的所有依赖项都被注入时仍然可以工作”

我更喜欢做的另一个选择是测试我的组件与它们所依赖的组件的交互。 如果我声明一个组件的依赖项,我想测试我的组件是否正确使用它。

恕我直言,一个设计良好的 API 允许我以我选择的方式工作和编码,一些技术限制是可以接受的。 我认为只有像 HttpClient 这样的组件的具体实现剥夺了我的选择权。

因此,在这些情况下,我只能选择采用我通常的解决方案,并创建一个轻量级的适配器/包装器库,它为我提供了一个接口并允许我按照我选择的方式工作。

我参加聚会也有点晚了,但是即使在阅读了所有前面的评论之后,我仍然不确定如何进行。 您能否告诉我如何验证被测对象中的特定方法调用公共 API 上的特定端点? 我不在乎什么回来。 我只是想确保当有人在我的 ApiClient 类的实例上调用“GoGetSomething”时,它会向“ https://somenifty.com/api/something ”发送一个 GET。 我想做的是(不工作的代码):

Mock<HttpClient> mockHttpClient = new Mock<HttpClient>();
mockHttpClient.Setup(x => x.SendAsync(It.IsAny<HttpRequestMessage>())).Verifiable();
ApiClient client = new ApiClient(mockHttpClient.Object);

Something response = await client.GoGetSomething();
mockHttpClient
    .Verify(x => x.SendAsync(
        It.Is<HttpRequestMessage>(m =>
            m.Method.Equals(HttpMethod.Get) &&
            m.RequestUri.Equals("https://somenifty.com/api/something"))));

但是,它不允许我模拟 SendAsync。

如果我能让那个基本的东西正常工作,那么我可能想实际测试我的类如何处理来自该调用的特定返回。

有任何想法吗? 这似乎(对我来说)不太像是要测试的不正常的东西。

但是,它不允许我模拟 SendAsync。

你不嘲笑HttpClient.SendAsync方法。

相反,您创建一个从HttpMessageHandler派生的模拟处理程序。 然后你重写SendAsync方法。 然后将该处理程序对象传递给HttpClient的构造函数。

@jdcrutchley ,您可以使用@richardszalay的 MockHttp 来做到这一点。

public class MyApiClient
{
    private readonly HttpClient httpClient;

    public MyApiClient(HttpMessageHandler handler)
    {
        httpClient = new HttpClient(handler, disposeHandler: false);
    }

    public async Task<HttpStatusCode> GoGetSomething()
    {
        var response = await httpClient.GetAsync("https://somenifty.com/api/something");
        return response.StatusCode;
    }
}

[TestMethod]
public async Task MyApiClient_GetSomething_Test()
{
    // Arrange
    var mockHandler = new MockHttpMessageHandler();
    mockHandler.Expect("https://somenifty.com/api/something")
        .Respond(HttpStatusCode.OK);

    var client = new MyApiClient(mockHandler);

    // Act
    var response = await client.GoGetSomething();

    // Assert
    Assert.AreEqual(response, HttpStatusCode.OK);
    mockHandler.VerifyNoOutstandingExpectation();
}

@jdcrutchley或者您使用一种方法创建您的 OWN 包装器类和包装器接口SendAsync .. 它只调用 _real_ httpClient.SendAsync (在您的包装器类中)。

我猜这就是@richardszalay的 MockHttp 正在做的事情。

这是人们在无法模拟某些东西时所做的一般模式(因为没有接口或者它不是虚拟的)。

mockhttp 是 HttpMessageHandler 上的模拟 DSL。 使用 Moq 动态模拟它没有什么不同,但由于 DSL,意图更清晰。

无论哪种方式,体验都是一样的:模拟 HttpMessageHandler 并从中创建一个具体的 HttpClient 。 您的域组件依赖于 HttpClient 或 HttpMessageHandler。

编辑:TBH,我不完全确定现有设计的问题是什么。 “mockable HttpClient”和“mockable HttpMessageHandler”之间的区别实际上是测试中的一行代码: new HttpClient(mockHandler) ,它的优点是实现为“每个平台的不同实现”和“保持相同的设计”用于测试的不同实现”(即,为了测试代码的目的没有被污染)。

像 MockHttp 这样的库的存在只是为了公开一个更干净、特定于域的测试 API。 通过接口使 HttpClient 可模拟不会改变这一点。

作为 HttpClient API 的新手,希望我的经验能为讨论增添价值。

我也很惊讶地发现 HttpClient 没有实现易于模拟的接口。 我在网上搜索,找到了这个帖子,并在继续之前从头到尾阅读了它。 @drub0y的回答说服我继续简单地模拟 HttpMessageHandler。

我选择的模拟框架是 Moq,对于 .NET 开发人员来说可能是相当标准的。 在花了将近一个小时与 Moq API 进行斗争以尝试设置和验证受保护的 SendAsync 方法后,我终于在验证受保护的泛型方法时遇到了 Moq 中的一个错误。 见这里

我决定分享这个经验来支持一个论点,即普通的 IHttpClient 接口会让生活变得更轻松,并为我节省了几个小时。 现在我遇到了死胡同,您可以将我添加到围绕 HttpClient 编写了一个简单的包装器/接口组合的开发人员列表中。

这是一个很好的观点,肯,我忘记了 HttpMessageHandler.SendAsync 受到保护。

尽管如此,我仍然赞赏 API 设计首先关注平台可扩展性,并且创建 HttpMessageHandler 的子类并不难(参见mockhttp 的示例)。 它甚至可以是一个子类,将调用移交给对 Mock 更友好的抽象公共方法。 我意识到这似乎“不纯”,但是您可以永远争论可测试性与 tainting-API-surface-for-testing-目的。

接口+1。

像其他人一样,我是 HttpClient 的新手,在课堂上需要它,开始搜索如何对其进行单元测试,发现了这个线程——现在几个小时后,我实际上不得不了解它的实现细节才能对其进行测试。 这么多人不得不走这条路的事实证明这对人们来说是一个真正的问题。 在少数发言的人中,还有多少人只是默默地阅读帖子?

我正在我的公司领导一个大型项目,根本没有时间去追寻这些兔子的踪迹。 一个界面可以节省我的时间和公司的资金——对于许多其他人来说可能也是如此。

我可能会走创建自己的包装器的路线。 因为从外面看,任何人都无法理解为什么我将 HttpMessageHandler 注入到我的班级中。

如果无法为 HttpClient 创建接口 - 那么也许 .net 团队可以创建一个具有接口的全新 http 解决方案?

@scott-martin, HttpClient什么都不做,他只是将请求发送到HttpMessageHandler

您可以测试和模拟HttpMessageHandler ,而不是测试HttpClient #$ 。 很简单,你也可以使用richardszalay mockhttp

@scott-martin 只提到了几条评论,你不注入 HttpMessageHandler。 你注入 HttpClient,并在单元测试时用 HttpMessageHandler 注入 _that_。

@richardszalay感谢您的提示,这确实可以更好地将低级抽象排除在我的实现之外。

@YehudahA ,我可以通过模拟 HttpMessageHandler 来让它工作。 但是“让它工作”并不是我插话的原因。正如@PureKrome所说,这是一个发现能力的问题。 如果有一个界面,我就可以模拟它并在几分钟内上路。 因为没有,我不得不花费几个小时来寻找解决方案。

我理解提供接口的局限性和犹豫。 我只是想作为社区成员提供我喜欢接口的反馈,并请给我们更多的反馈——它们更容易交互和测试。

这是一个发现能力的问题。 如果有一个界面,我就可以模拟它并在几分钟内上路。 因为没有,我不得不花费几个小时来寻找解决方案。

:arrow_up: 这个。 都是这样的,

编辑:强调,我的。

虽然我完全同意它可能更容易被发现,但我不确定它如何需要“几个小时”。 谷歌搜索“mock httpclient”将这个Stack Overflow 问题作为第一个结果,其中两个评分最高的答案(虽然,授予,而不是接受的答案)描述了 HttpMessageHandler。

NP - 我们仍然同意不同意。 这一切都很好:)

你可以关闭这个问题,因为 mscorlib 带着它所有的包袱回来了 :)

我们可以让这些 httpclient 类实现一个接口吗? 然后可以自己处理剩下的事情。

您建议使用哪种解决方案来模拟 DI 服务中使用的 HttpClient ?
我正在尝试实现 FakeHttpClientHandler,但我不知道如何处理 SendAsync 方法。

我有一个简单的 GeoLocationService,它调用一个微服务来从另一个微服务中检索基于 ip 的位置信息。 我想将伪造的 HttpClientHandler 传递给服务,该服务有两个构造函数,一个接收 locationServiceAddress,一个接收 locationServiceAddress 和附加的 HttpClientHandler。

public class GeoLocationService: IGeoLocationService {

        private readonly string _geoLocationServiceAddress;
        private HttpClient _httpClient;
        private readonly object _gate = new object();

        /// <summary>
        /// Creates a GeoLocation Service Instance for Dependency Injection
        /// </summary>
        /// <param name="locationServiceAddress">The GeoLocation Service Hostname (IP Address), including port number</param>
        public GeoLocationService(string locationServiceAddress) {
            // Add the ending slash to be able to GetAsync with the ipAddress directly
            if (!locationServiceAddress.EndsWith("/")) {
                locationServiceAddress += "/";
            }

            this._geoLocationServiceAddress = locationServiceAddress;
            this._httpClient = new HttpClient {
                BaseAddress = new Uri(this._geoLocationServiceAddress)
            };
        }

        /// <summary>
        /// Creates a GeoLocation Service Instance for Dependency Injection (additional constructor for Unit Testing the Service)
        /// </summary>
        /// <param name="locationServiceAddress">The GeoLocation Service Hostname (IP Address), including port number.</param>
        /// <param name="clientHandler">The HttpClientHandler for the HttpClient for mocking responses in Unit Tests.</param>
        public GeoLocationService(string locationServiceAddress, HttpClientHandler clientHandler): this(locationServiceAddress) {
            this._httpClient.Dispose();
            this._httpClient = new HttpClient(clientHandler) {
                BaseAddress = new Uri(this._geoLocationServiceAddress)
            };
        }

        /// <summary>
        /// Geo Location Microservice Http Call with recreation of HttpClient in case of failure
        /// </summary>
        /// <param name="ipAddress">The ip address to locate geographically</param>
        /// <returns>A <see cref="string">string</see> representation of the Json Location Object.</returns>
        private async Task<string> CallGeoLocationServiceAsync(string ipAddress) {
            HttpResponseMessage response;
            string result = null;

            try {
                response = await _httpClient.GetAsync(ipAddress);

                response.EnsureSuccessStatusCode();

                result = await response.Content.ReadAsStringAsync();
            }
            catch (Exception ex) {
                lock (_gate) {
                    _httpClient.Dispose(); 
                    _httpClient = new HttpClient {
                        BaseAddress = new Uri(this._geoLocationServiceAddress)
                    };
                }

                result = null;

                Logger.LogExceptionLine("GeoLocationService", "CallGeoLocationServiceAsync", ex.Message, stacktrace: ex.StackTrace);
            }

            return result;
        }

        /// <summary>
        /// Calls the Geo Location Microservice and returns the location description for the provided IP Address. 
        /// </summary>
        /// <param name="ipAddress">The <see cref="string">IP Address</see> to be geographically located by the GeoLocation Microservice.</param>
        /// <returns>A <see cref="string">string</see> representing the json object location description. Does two retries in the case of call failure.</returns>
        public async Task<string> LocateAsync(string ipAddress) {
            int noOfRetries = 2;
            string result = "";

            for (int i = 0; i < noOfRetries; i ++) {
                result = await CallGeoLocationServiceAsync(ipAddress);

                if (result != null) {
                    return result;
                }
            }

            return String.Format(Constants.Location.DefaultGeoLocationResponse, ipAddress);
        }

        public void Dispose() {
            _httpClient.Dispose();
        }

    }

在测试类中,我想编写一个名为 FakeClientHandler 的内部类,它扩展了 HttpClientHandler,覆盖了 SendAsync 方法。 我猜另一种方法是使用模拟框架来模拟 HttpClientHandler。 我还不知道该怎么做。 如果你能帮忙,我将永远欠你的债。

public class GeoLocationServiceTests {

        private readonly IConfiguration _configuration;
        private IGeoLocationService _geoLocationService;
        private HttpClientHandler _clientHandler;

        internal class FakeClientHandler : HttpClientHandler {

            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {

                // insert implementation here

                // insert fake responses here. 
                return (Task<HttpResponseMessage>) null;
            }

        }

        public GeoLocationServiceTests() {
            this._clientHandler = new FakeClientHandler();
            this._clientHandler.UseDefaultCredentials = true;
            this._geoLocationService = new GeoLocationService("http://fakegeolocation.com/json/", this._clientHandler);
        }


        [Fact]
        public async void RequestGeoLocationFromMicroservice() {
            string ipAddress = "192.36.1.252";

            string apiResponse = await this._geoLocationService.LocateAsync(ipAddress);
            Dictionary<string, string> result = JsonConvert.DeserializeObject<Dictionary<string,string>>(apiResponse);

            Assert.True(result.ContainsKey("city"));

            string timeZone;
            result.TryGetValue("time_zone", out timeZone);

            Assert.Equal(timeZone, @"Europe/Stockholm");
        }

        [Fact]
        public async void RequestBadGeoLocation() {
            string badIpAddress = "asjldf";

            string apiResponse = await this._geoLocationService.LocateAsync(badIpAddress);
            Dictionary<string, string> result = JsonConvert.DeserializeObject<Dictionary<string, string>>(apiResponse);

            Assert.True(result.ContainsKey("city"));

            string city;
            result.TryGetValue("city", out city);
            Assert.Equal(city, "NA");

            string ipAddress;
            result.TryGetValue("ip", out ipAddress);
            Assert.Equal(badIpAddress, ipAddress);
        }
    }

@danielmihai HttpMessageHandler 实际上是“实现”。 您通常创建一个 HttpClient,将处理程序传递给构造函数,然后让您的服务层依赖于 HttpClient。

使用通用模拟框架很困难,因为可覆盖的 SendAsync 受到保护。 出于这个原因,我专门编写了mockhttp (但也因为拥有特定于域的模拟功能很好。

@danielmihai理查德所说的几乎总结了我们现在必须忍受的模拟 HttpClient 的痛苦。

还有HttpClient.Helpers作为 _another_ 库,它可以帮助您模拟 HttpClient .. 或者更具体地说,为您提供假的HttpMessageHandler来伪造响应。

我想通了,写了这样的东西...... [我想我已经处理了“痛苦”]

internal class FakeClientHandler : HttpClientHandler {
            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
                if (request.Method == HttpMethod.Get) {
                    if (request.RequestUri.PathAndQuery.Contains("/json/")) {
                        string requestPath = request.RequestUri.PathAndQuery;
                        string[] splitPath = requestPath.Split(new char[] { '/' });
                        string ipAddress = splitPath.Last();

                        // RequestGeoLocationFromMicroservice
                        if (ipAddress.Equals(ipAddress1)) {
                            var response = new HttpResponseMessage(HttpStatusCode.OK);
                            response.Content = new StringContent(response1);

                            return Task.FromResult(response);
                        }

                        // RequestBadGeoLocation
                        if (ipAddress.Equals(ipAddress2)) {
                            var response = new HttpResponseMessage(HttpStatusCode.OK);
                            response.Content = new StringContent(response2);
                            return Task.FromResult(response);
                        }
                    }
                }
                return (Task<HttpResponseMessage>) null;
            }
        }

所有测试通过

如果有更好的方法,请对实施发表评论。

谢谢!

将我的想法添加到这个一年多的讨论中。

@SidharthNabar

首先,我注意到您正在创建新的 HttpClient 实例来发送每个请求 - 这不是 HttpClient 的预期设计模式。 创建一个 HttpClient 实例并将其重用于所有请求有助于优化连接池和内存管理。 请考虑重用 HttpClient 的单个实例。 完成此操作后,您可以将假处理程序插入到 HttpClient 的一个实例中,然后就完成了。

当我不应该使用using语句,而是在我的 HttpClient 实例(例如在消费者的构造函数中)使用new时,我应该在哪里处理未注入的实例? 我为注入 HttpClient 实例而构建的 HttpMessageHandler 呢?

除了完整的接口或不讨论之外(我完全支持接口),至少文档可以说明您可以注入 HttpMessageHandler 用于测试目的。 是的,提到了构造函数,是的,提到了你可以注入你自己的HttpMessageHandler,但是没有说明我为什么要这样做。

@richardszalay

虽然我完全同意它可能更容易被发现,但我不确定它如何需要“几个小时”。 谷歌搜索“mock httpclient”将这个 Stack Overflow 问题作为第一个结果,其中两个评分最高的答案(虽然,授予,而不是接受的答案)描述了 HttpMessageHandler。

API 应该直观地用于浏览文档,恕我直言。 不得不问 Google 或 SO 问题当然不直观,可以追溯到 .NET Core 之前的链接问题使得最新性不明显(可能在问题和 _new_ 框架的发布之间发生了一些变化?)

感谢所有添加代码示例并提供帮助程序库的人!

我的意思是,如果我们有一个需要进行 http 调用的类,并且我们想要对该类进行单元测试,那么我们必须能够模拟那个 http 客户端。 由于 http 客户端没有接口,我无法为该客户端注入模拟。 相反,我必须为接受消息处理程序作为可选参数的 http 客户端构建一个包装器(实现一个接口),然后向它发送一个假处理程序并注入它。 这是一大堆额外的箍来跳过。 另一种方法是让我的班级采用 HttpMessageHandler 的实例而不是 HttpClient,我猜...

我不是要分头发,只是感觉很尴尬,似乎有点不直观。 我认为这是一个如此受欢迎的话题,这一事实支持了这一点。

由于 http 客户端没有接口,我们不能用容器注入它。

@dasjestyr我们可以在没有接口的情况下进行依赖注入。 接口支持多重继承; 它们不是依赖注入的要求。

是的,DI 不是这里的问题。 模拟它是多么容易/好,等等。

@shaunluttin我知道我们可以在没有接口的情况下进行 DI,我没有暗示其他方式; 我说的是一个依赖于 HttpClient 的类。 如果没有客户端接口,我无法注入模拟的 http 客户端或任何 http 客户端(以便可以在不进行实际 http 调用的情况下对其进行隔离测试),除非我将其包装在实现我的接口的适配器中已经定义为模仿 HttpClient 的。 为了清楚起见,我稍微更改了一些措辞,以防您只关注一个句子。

与此同时,我一直在我的单元测试中创建一个假消息处理程序并注入它,这意味着我的类是围绕注入一个处理程序而设计的,我认为这不是世界上最糟糕的事情,但感觉很尴尬。 并且测试需要假货而不是模拟/存根。 总的。

另外,从学术上讲,接口不支持多重继承,它们只允许你模仿它。 你不继承接口,你实现它们——你不是通过实现接口来_继承_任何功能,只是声明你已经以某种方式实现了功能。 为了以其他方式模仿多重继承和您在其他地方编写的一些现有功能,您可能会实现接口并将实现通过管道传递给包含实际功能(例如适配器)的内部成员,该成员实际上是组合,而不是继承。

与此同时,我一直在我的单元测试中创建一个假消息处理程序并注入它,这意味着我的类是围绕注入一个处理程序而设计的,我认为这不是世界上最糟糕的事情,但感觉很尴尬。 并且测试需要假货而不是模拟/存根。 总的。

这是这个问题的核心/核心 -> 它也是_purely_ 自以为是的。

栅栏的一侧:假消息处理程序,因为<insert their rational here>
栅栏的另一面:接口(甚至是虚拟方法,但这仍然是一个很小的 ​​PITA),因为<insert their rational here>

好吧,我的意思是假冒需要大量额外的工作来设置,并且在很多情况下会导致你编写一个完整的其他类只是为了测试而不会真正增加很多价值。 存根很棒,因为它允许测试在没有真正实现的情况下继续进行。 模拟是我真正感兴趣的,因为它们可以用于断言。 例如,断言此接口实现上的方法在此测试的当前设置下被调用了 x 次。

实际上,我不得不在我的一门课上进行测试。 为此,我必须在每次调用 SendAsync() 方法时递增的假处理程序中添加一个累加器。 这是一个简单的案例,谢天谢地,但来吧。 在这一点上,模拟实际上是一个标准。

是的 - 在这里完全同意你的观点👍🍰

请微软添加IHttpClient句点。 或者保持一致并删除 BCL 中的所有接口 - IListIDictionaryICollection ,因为它们无论如何都“难以版本化”。 哎呀,也摆脱IEnumerable

严肃地说,不要争论“你可以只写你自己的处理程序”(当然,但是界面更容易和更干净)和“界面引入了重大变化”(好吧,无论如何,你在所有 .net 版本中都这样做) ,只需添加接口,让开发人员决定他们想如何测试它。

@lasseschou IListIDictionaryICollectionIEnumerable绝对至关重要,因为实现它们的类种类繁多。 IHttpClient只能由HttpClient实现 - 为了与您的逻辑保持一致,BCL 中的每个类都必须指定一个接口类型,因为我们可能需要模拟它。
看到这种混乱,我会很难过。

除非有一个戏剧性的权衡,否则IHttpClient无论如何都是错误的抽象级别来模拟。 您的应用程序几乎肯定不会使用 HttpClient 的全部功能。 我建议您根据应用程序的特定请求将HttpClient包装在您自己的服务中,并模拟您自己的服务。

@jnm2 - 出于兴趣,您为什么认为IHttpClient是要模拟的错误抽象级别?

我不是讨厌或拖钓-诚实的Q。

(喜欢学习等)。

考虑到它几乎是一个完美的抽象级别,因为它是一个客户端,所以我会问同样的问题。

这不是绝对的。 如果您正在构建 Web 浏览器,那么我可以看到您的应用程序需要的内容和 HttpClient 抽象的内容之间的比例是 1:1。 但是,如果您使用 HttpClient 与某种 API 通信——在 HTTP 之上有更具体的期望的任何东西——你只需要 HttpClient 可以做的一小部分。 如果您构建一个封装 HttpClient 传输机制的服务并模拟该服务,那么您的应用程序的其余部分可以针对您的服务的 API 特定接口抽象进行编码,而不是针对 HttpClient 进行编码。

另外,我赞同“永远不要模拟你不拥有的类型”的原则,所以 YMMV。
另请参阅http://martinfowler.com/articles/mocksArentStubs.html比较经典测试与模拟。

这里的重点是 HttpClient 是一个巨大的进步
WebClient,XMLHttpRequest,它已成为做 HTTP 的首选方式
来电。 它干净而现代。 我总是使用会说话的可模拟适配器
直接到 HttpClient,所以测试我的代码很容易。 但我想
也测试那些适配器类。 就在那时我发现:没有
IHttpClient。 也许你是对的,它不需要。 也许你认为它是
错误的抽象级别。 但是不这样真的很方便
必须在所有项目中将其包装在另一个类中。 我只是没看到
任何理由不为如此重要的类提供单独的接口。

Lasse Schou
创始人兼首席执行官

http://mouseflow.com
[email protected]

https://www.facebook.com/mouseflowdotcom/
https://twitter.com/mouseflow
https://www.linkedin.com/company/mouseflow

保密声明:此电子邮件信息(包括任何附件)是
仅供预定收件人使用,可能包含机密信息,
受法律保护的专有和/或特权信息。 任何
禁止未经授权的审查、使用、披露或分发。 如果你
不是预定的收件人,请通过回复电子邮件与发件人联系
并销毁原始消息的所有副本。

2016 年 11 月 12 日 14:57,Joseph Musser [email protected]
写道:

这不是绝对的。 如果您正在构建网络浏览器,那么我可以看到
您的应用程序需要的内容和 HttpClient 抽象的内容之间是如何 1:1 的。 但
如果您正在使用 HttpClient 与某种 API 对话——任何与
在 HTTP 之上的更具体的期望,你只需要一个
HttpClient 可以做的事情的一小部分。 如果你建立一个服务
封装 HttpClient 传输机制并模拟该服务,
您的应用程序的其余部分可以针对您的服务的 API 特定接口进行编码
抽象,而不是针对 HttpClient 进行编码。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/dotnet/corefx/issues/1624#issuecomment -260123552,或者静音
线程
https://github.com/notifications/unsubscribe-auth/ACnGPp3fSriEJCAjXeAqMZbBcTWcRm9Rks5q9cXlgaJpZM4EPBHy
.

我听到一些奇怪的输入,即 HttpClient “太灵活”而不能成为正确的模拟层——这似乎更像是决定让一个类具有如此多的职责和暴露的特性的副作用。 但这真的与人们_想要_如何测试代码无关。

我仍然非常希望我们能解决这个问题,只是因为我们通过期望消费者知道这些 HttpClient 是如何实现的细节来打破封装(在这种情况下,要“知道”它是一个没有添加有意义功能的包装器)并包装他们可能使用或不使用的另一个类)。 框架使用者不应该知道关于实现的任何事情,而不是确切地向他们公开什么接口,这里的很多合理化是我们有一个解决方法,并且对类的深入熟悉提供了许多封装不好的方法模拟/存根功能。

请修复!

只是偶然发现了这个,所以把我的 2c 扔进去了......

我认为嘲笑 HttpClient 是愚蠢的。

即使只有一个SendAsync可以模拟,但HttpResponseMessageHttpResponseHeadersHttpContentHeaders等有很多细微差别,你的模拟将会很快就会变得一团糟,而且可能也表现得不对。

我? 我使用OwinHttpMessageHandler

  1. 从服务外部执行 HTTP 验收测试。
  2. 将 _Dummy_ HTTP 服务注入想要向所述服务发出出站 HTTP 请求的组件中。 也就是说,我的组件有一个HttpMessageHandler的可选 ctor 参数。

我确定有一个类似的 MVC6 处理程序......

_生活很好。_

@damianh :为什么使用 dotnet core 的人需要对 Owin 做任何事情来测试像 HttpClient 这样的类? 这对于特定用例(托管在 OWIN 上的 AspCore)可能很有意义,但这似乎并不那么普遍。

这似乎确实将“什么是最通用的单元测试 HttpClient 方法也符合用户期望”的关注与“我如何集成测试”:) 混为一谈。

哦 _testing_ HttpClient... 以为主题是测试具有
依赖于 HttpClient。 别管我。 :)

2016 年 12 月 5 日晚上 7:56,“brphelps” [email protected]写道:

@damianh https://github.com/damianh :人们为什么要使用 dotnet core
需要用 Owin 做任何事情来测试像 HttpClient 这样的类吗? 这有可能
对于特定用例(托管在 OWIN 上的 AspCore)很有意义,但是
这似乎不是那么普遍。

这似乎真的混淆了“什么是最普遍的
也符合用户期望的单元测试 HttpClient 的方法”
与“我如何集成测试”:)。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/dotnet/corefx/issues/1624#issuecomment-264942511或静音
线程
https://github.com/notifications/unsubscribe-auth/AADgXJxunxBgFGLozOsisCoPqjMoch48ks5rFF5vgaJpZM4EPBHy
.

@damianh - 这是具有依赖关系的事物,但您的解决方案似乎也涉及图片中的许多其他依赖关系。 我在想象一个恰好使用传入的 HttpClient 实例(或 DI 实例)的类库——Owin 在哪里出现?

这不是我想要展示的有趣的东西(
也适用于.net core / netstandard),所以请不要专注于此。

有趣的是 HttpMessageHandler 使进程内
针对虚拟 HTTP 端点的内存中 HTTP 请求。

Aspnet Core 的 TestServer 和之前的 katana 一样,工作方式相同
大大地。

所以你的测试类有一个 HttpClient DI'd _it_ 有一个
HttpMessageHandler 直接注入其中。 可测试和
可断言的表面就在那里。

模拟,一种特定形式的测试替身,我仍然建议
反对 HttpClient。 因此,不需要 IHttpClient。

2016 年 12 月 5 日晚上 8:15,“brphelps” [email protected]写道:

@damianh https://github.com/damianh -- 这是有
依赖项,但您的解决方案似乎涉及许多其他依赖项
在图片中,也是。 我正在想象一个碰巧使用的类库
传入 HttpClient 实例(或 DI 的实例)-- Owin 进入何处
图片?


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/dotnet/corefx/issues/1624#issuecomment-264947538或静音
线程
https://github.com/notifications/unsubscribe-auth/AADgXEESfQj9NnKLo8LucFLyajWtQXKBks5rFGK8gaJpZM4EPBHy
.

@damianh——完全理解你在说什么,有趣的是——但人们不需要带有智能模拟的虚拟 HTTP 端点——例如@richardszalay的 MockHttp 库。

至少不适用于单元测试:)。

这取决于。 选项很好:)

2016 年 12 月 5 日晚上 8:39,“brphelps” [email protected]写道:

@damianh https://github.com/damianh -- 完全理解你是什么
说有趣的是——但人们不需要虚拟 HTTP
带有智能模拟的端点——例如@richardszalay
https://github.com/richardszalay的 MockHttp 库。

至少不适用于单元测试:)。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/dotnet/corefx/issues/1624#issuecomment-264954210或静音
线程
https://github.com/notifications/unsubscribe-auth/AADgXOl4UEDGYCVLpbvwhueaK52VtjH6ks5rFGhlgaJpZM4EPBHy
.

@damianh说:
选项很好:)

是的 - 完全同意👍哪个,IMO ..我们现在没有😢 **

我完全理解创建和注入自己的 HMH 的合理性。 这就是我现在正在做的事情。 _不幸的是_,我必须创建第二个 ctor(或提供可选的 ctor 参数)_用于单元测试问题_。 再想想那句话->我正在创建一个完全可能的用户选项......这实际上只是为了启用单元测试来_做一些事情_。 我班的最终用户/消费者永远不会_真的_使用那个 ctor 选项......这对我来说很臭。

回到关于选项的话题-> 现在我觉得我们没有选项。 我们_有_到_学习_关于HttpClient的内部结构。 从字面上打开引擎盖,看看引擎盖下。 (我很幸运得到了一个 TL;DR; 来自福勒爵士的真棒™️ 这减少了我的研究时间)。

所以 - 关于你关于无接口的观点,你能否提供一些接口什么时候是坏事的例子,好吗? 等等。记住->我一直在将接口作为小/简单到中等示例的_option_推广......不是所有示例的 100%。

我不是想拖钓或攻击你达米安——只是想理解。

* Ninja 编辑:从技术上讲,我们确实有选项 -> 我们无能为力/创建自己的包装器/等等。我的意思是:没有多少选项可以简化事情,*out-of-the-box

@PureKrome ,不用担心只是分享经验。

术语“单元测试”已被多次使用,让我们考虑一下。 所以我们在同一页上,我假设我们有一个FooClassHttpClient (或HMH )有一个ctor依赖,我们想要_mock_ HttpClient

我们在这里真正要说的是“ FooClass可以使用任何方法、任何内容类型、任何传输编码、任何内容编码、_any 向任何 URL(协议/主机/资源)发出 HTTP 请求request headers_,并且可以处理任何响应内容类型、任何状态代码、cookie、重定向、传输编码、缓存控制标头等以及网络超时、任务取消异常”。

HttpClient就像一个上帝对象; 你可以用它做很多事情。 HTTP 也是一个超出您的进程的远程服务调用。

这真的是单元测试吗? 现在说实话:)

** Ninja Edit too:如果你想让FooClass单元可测试,它想从服务器获取一些 json,它会依赖于Func<CancellationToken, Task<JObject>>或类似的ctor。

你能提供一些接口什么时候是坏事的例子吗?

没这么说! 我说IHttpClient不会有用,因为 A)它不会被替代的具体实现替代,并且 B)如上所述的上帝对象问题。

另一个例子? IAssembly

@damianh—— “神级”状态不是呼叫者关心的问题。 确保 HttpClient 不是上帝类不是调用者的责任。 他们知道这是他们用来进行 HTTP 调用的对象 :)。

所以,假设我们接受 .NET 框架向我们公开了一个上帝类……我们如何才能减少我们的担忧? 我们怎样才能最好地封装它? 这很容易——使用传统的模拟框架(Moq 是一个很好的例子)并完全隔离 HttpClient 实现,因为作为人们使用它,我们只是不关心它的内部设计。 我们只关心我们如何使用它。 这里的问题是传统方法不起作用,因此,您会看到人们提出越来越多有趣的解决方案来解决问题:)。

语法和清晰度的一些编辑:D

完全同意@damianhHttpClient就像一些超级上帝班。

我也不明白为什么我需要再次了解所有内部信息,因为对于输入的内容和输出的内容可能会有_如此多的变化。

例如:我想从某个 API 中获取股票。 https://api.stock-r-us.com/aapl 。 结果是一些 JSON Paylod。

所以,当我试图调用那个端点时,我可能会搞砸:

  • 错误的端点网址?
  • 错误的方案?
  • 缺少特定的标题?
  • 不正确的标题
  • 不正确的请求有效负载(也许这是一个带有某些正文的 POST?)
  • 网络错误/超时/等
    ...

所以我明白,如果我想测试这些东西,HMH 是一个很好的方法,因为它只是劫持了_real_ HMH 并替换了我所说的返回。

但是——如果我想(自担风险)忽略所有这些事情并说:
_想象一下我想打电话和端点,一切都很好,我得到了我漂亮的 json 有效负载结果。_
我还应该学习和担心所有这些东西,做这么简单的事情吗?

或者……这是一种糟糕的思维方式。 我们不应该这样想,因为它太hacky了? 太便宜? 太容易出错?

大多数时候我只是调用一个端点。 我不考虑诸如标头或缓存控制或网络错误之类的事情。 我通常只考虑 VERB + URL(和 PAYLOAD,如果需要),然后是 RESULT。 我对此进行了错误处理,以 Catch-All-The-Things :tm: 然后承认失败,因为发生了一些不好的事情 :(

所以我要么看错了问题(即我仍然无法动摇我的 n00b 状态) - 要么 - 这个神级正在给我们带来一堆单元测试的悲痛。

HTTP 也是一个超出您的进程的远程服务调用。
这真的是单元测试吗? 现在说实话:)

对我来说, HttpClient是一个_依赖服务_。 我不在乎_服务真正做了什么,我把它归类为_做一些我的代码库外部的东西_。 因此,我不希望它使用外部电源来做事,所以我想控制它,劫持它并确定这些服务调用的结果。 我仍在尝试测试 _my_ 工作单元。 我的小逻辑。 当然,这可能取决于其他东西。 如果我可以控制整个宇宙,这仍然是一个单元测试,这条逻辑存在于_并且_我没有依赖/集成其他服务。

** 另一个编辑:或者也许-如果 HttpClient 实际上是一个上帝对象,也许对话应该围绕将其简化为…​​…更好? 或者,它根本就不是神物? (只是试图打开辩论)..

不幸的是,我必须为单元测试问题创建第二个 ctor(或提供可选的 ctor 参数)。 再想一想那句话-> 我正在创建一个完全可能的 USER OPTION ...这实际上只是为了使单元测试能够做一些事情。 我班的最终用户/消费者永远不会真正使用那个 ctor 选项......对我来说闻起来很臭

我不确定你在这里想说什么:

“我不喜欢有第二个接受 HttpMessageHandler 的 ctor”......所以让你的测试将它包装在 HttpClient 中

“我想在不反转对 HttpClient 的依赖的情况下进行单元测试”......很难吗?

@richardszalay - 我想我得到@PureKrome不想以一种不会改进设计的方式更改他的代码只是为了支持过于固执己见的测试策略的情绪。 不要误会我的意思,我不是在抱怨你的库,我想要一个更好的解决方案的唯一原因是因为我对 dotnet core 有更高的期望,不需要它 =)。

我不是从 MockHttp 的角度考虑这一点,而是在一般情况下进行测试/模拟。
反向依赖是 .NET 中单元测试的基本必要条件,这通常涉及到 ctor-injection。 我不清楚提议的替代方案是什么。

当然,如果 HttpClient 实现了 IHttpClient,那么仍然会有一个构造函数接受它......

当然,如果 HttpClient 实现了 IHttpClient,那么仍然会有一个构造函数接受它......

同意。 当我刚刚醒来并努力让我大脑中的单个细胞活跃起来时,我的思维正在“穿越溪流”。

请忽略我的“ctor”评论并继续讨论,如果有人还没有无聊地离开😄

如果使用HttpClient作为依赖项真的_可以_导致单元测试,我们可以退后一步吗?

假设我有一个类FooClass注入了依赖构造函数。 我现在想对FooClass上的方法进行单元测试,这意味着我指定了我的方法的输入,我尽可能地将FooClass与其环境隔离,并根据预期值检查结果. 在所有这些管道中,我不想关心我的依赖对象的内脏; 如果我可以指定我的FooClass应该存在的环境类型,那对我来说是最好的:我想模拟对我的测试系统的依赖,它的行为完全符合我的需要让我的测试工作。

现在我必须关心HttpClient的实现方式,因为我必须为我的注入依赖项( HttpClient )为我的测试系统指定一个依赖项( HttpMessageHandler )。 我不仅要模拟我的直接依赖,还必须使用模拟的链式依赖创建它的具体实现。

主题:我无法在不知道 HttpClient 内部如何工作的情况下模拟 HttpClient,而且我还必须依靠它的实现来做任何令人惊讶的事情,因为我无法通过模拟完全绕过类的内部。 这基本上不遵循单元测试 101 - 从测试中获取不相关的依赖代码 =)。

您可以在许多不同的评论中看到主题:

“对我来说,HttpClient 是一个依赖服务。我不在乎该服务的真正作用,我将其归类为在我的代码库之外做一些事情的东西。”——PureKrome

“现在我必须关心 HttpClient 的实现方式,因为我必须为被测系统的注入依赖项 (HttpClient) 指定一个依赖项 (HttpMessageHandler)。” -- Thaoden

“考虑到它几乎是一个完美的抽象水平,因为它是一个客户,所以我会问同样的问题。” ——达斯提尔

“HttpMessageHandler 实际上是“实现”。您通常创建一个 HttpClient,将处理程序传递给构造函数,然后让您的服务层依赖于 HttpClient。 ——理查德扎莱

“但关键问题仍然存在——您需要定义一个假处理程序并将其插入到 HttpClient 对象下方。” ——西哈特纳巴尔

“HttpClient 有很多用于单元可测试性的选项。它不是密封的,它的大多数成员都是虚拟的。它有一个可用于简化抽象的基类以及 HttpMessageHandler 中精心设计的扩展点”——ericstj

“如果 API 不可模拟,我们应该修复它。但它与接口与类无关。从可模拟性的角度来看,接口与纯抽象类没有什么不同,出于所有实际目的。” ——克日什托夫·克劳利纳

“恕我直言,如果选择必须编写自己的伪造消息处理程序,这在不深入研究代码的情况下并不明显,那么这是一个非常高的障碍,会让许多开发人员感到非常沮丧。” ——克托尔金

这真的是单元测试吗? 现在说实话:)

我不知道。 测试是什么样的?

我认为这种说法有点草率。 关键是要模拟作为 http 客户端的依赖项,以便代码可以与这些依赖项隔离运行……换句话说,实际上并不进行 http 调用。 我们要做的就是确保接口上的预期方法在正确的条件下被调用。 我们是否应该选择更轻的依赖似乎是一个完全不同的讨论。

HttpClient 是有史以来最糟糕的依赖。 它从字面上说明
依赖者可以“调用互联网”。 当然,接下来你们都在嘲笑 TCP。 呵呵

2016 年 12 月 7 日上午 5:34,“Jeremy Stafford” [email protected]写道:

这真的是单元测试吗? 现在说实话:)

我不知道。 测试是什么样的?

我认为这种说法有点草率。 关键是嘲笑
依赖项是 http 客户端。 我们要做的就是确保
接口上的预期方法在右边被调用
状况。 我们是否应该选择更轻的依赖似乎
是一个完全不同的讨论。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/dotnet/corefx/issues/1624#issuecomment-265353526或静音
线程
https://github.com/notifications/unsubscribe-auth/AADgXPL39vnUbWrUuUBtfmbjhk5IBkxHks5rFjdqgaJpZM4EPBHy
.

这基本上不遵循单元测试 101 - 从测试中获取不相关的依赖代码 =)。

就个人而言,我认为这不是对特定实践的正确解释。 我们不是在尝试测试依赖项,而是在尝试测试我们的代码如何与该依赖项交互,因此它不是不相关的。 而且,很简单,如果我不能在不调用实际依赖项的情况下对我的代码运行单元测试,那么这不是孤立的测试。 例如,如果该单元测试是在没有 Internet 访问的构建服务器上运行的,那么测试可能会中断——因此希望模拟它。 您可以删除对 HttpClient 的依赖,但这只是意味着它最终会出现在您编写的某个其他类中,从而进行调用——也许我正在谈论那个类? 你不知道,因为你从来没有问过。 那么我的域服务应该依赖于 HttpClient 吗? 不,当然不。 它会被抽象为某种其他类型的抽象服务,而我会模拟它。 好的,所以在注入我提出的任何抽象的层中——在链的某个地方,_something_ 将了解 HttpClient,并且某些东西将被我编写的其他东西包裹,而其他东西就是我的东西'我还是要写一个单元测试。

因此,如果我对某种调解器的单元测试是测试在 Internet 上对服务的不成功调用会导致我的代码回滚某种形式的事务,而对服务的成功调用会导致提交该事务事务,然后我需要控制该调用的结果,以便设置该测试的条件,以便我可以断言这两种情况的行为都是正确的。

我可以理解一些人认为 HttpClient 不是正确的抽象级别的观点,但是我认为没有人询问消费类的“级别”,这是一种假设。 可以将 HTTP 调用抽象为一个服务和接口,但是我们中的一些人仍然想测试我们的 HTTP 调用的“包装器”是否按预期运行,这意味着我们需要控制响应。 目前,这可以通过给 HttpClient 一个假的处理程序来实现,这就是我现在正在做的事情,这很好,但关键是如果你想要那个覆盖范围,你将不得不在某个时候使用 HttpClient至少伪造响应。

但是,现在让我退后一步。 我看的越多,它对于可模拟的界面(已经说过)来说太胖了。 我认为这可能是源于“客户”这个名字的心理问题。 我并不是说我认为这是错误的,而是它与现在人们可能在野外看到的其他“客户端”实现有点不一致。 我们今天看到很多“客户端”实际上是服务门面,所以当人们看到 Http CLIENT 这个名字时,他们可能会想到同样的事情,你为什么不模拟一个服务,对吧?

我现在看到处理程序是对代码很重要的部分,所以我将围绕它进行设计。

基本上,我认为我们模拟 HttpClient 的愿望可能与普通开发人员倾向于创建新的 HttpClient 实例而不是重用它们的倾向有关,这是一种糟糕的做法。 换句话说,我们低估了 HttpClient 是什么,而应该专注于处理程序。 此外,处理程序是抽象的,因此可以轻松模拟。

好吧,我被卖了。 我不再希望 HttpClient 上的接口用于模拟。

在挣扎了很久之后,我制作了自己的 IHttpHandler 接口并为我的用例实现了一切。 使我的具体代码更易于阅读,并且可以在没有一些令人毛骨悚然的东西的情况下模拟测试。

今天遇到了这个: http ://www.davesquared.net/2011/04/dont-mock-types-you-dont-own.html

一旦您需要在每个请求上发送不同的 HTTP 标头(在与正确设计的 RESTful Web 服务进行通信时,这一点至关重要),几乎不可能共享任何真正的应用程序的 HttpClient 实例。 目前HttpRequestHeaders DefaultRequestHeaders绑定到实例,而不是有效地使其有状态的调用。 相反, {Method}Async()应该接受它,这将使 HttpClient 无状态并且真正可重用。

@abatishchev但是您可以在每个 HttpRequestMessage 上指定标头。

@richardszalay我并不是说这完全不可能,我说 HttpClient 不是为此目的而设计的。 {Method}Async()都不接受HttpRequestMethod ,只有SendAsync()接受。 但是其余的目的是什么?

但是其余的目的是什么?

满足 99% 的需求。

这是否意味着设置标题占用例的 1%? 我怀疑。

无论哪种方式,如果这些方法具有接受 HttpResponseMessage 的重载,这都不是问题。

@abatishchev我不怀疑,但是无论哪种方式,如果我发现自己处于您的场景中,我都会编写扩展方法。

我玩弄了可能连接 HttpMessageHandler 和可能的 HttpRequestMessage 的想法,因为我不喜欢编写假货(相对于模拟)。 但是越往下走,您就越会意识到您将试图伪造实际的数据值对象(例如 HttpContent),这是徒劳的。 因此,我认为将您的依赖类设计为可选地将 HttpMessageHandler 作为 ctor 参数并使用假的进行单元测试是最合适的路线。 我什至认为包装 HttpClient 也是浪费时间......

这将允许您对依赖类进行单元测试,而无需实际调用 Internet,这正是您想要的。 您的假货可以返回预先确定的状态代码和内容,以便您可以测试您的依赖代码是否按预期处理它们,这又是您真正想要的。

@dasjestyr您是否尝试为HttpClient创建一个接口(这就像为其创建一个包装器)而不是为HttpMessageHandlerHttpRequestMessage的接口。

/我很好奇。

@PureKrome我草拟了为它创建一个界面的草图,这就是我很快意识到它毫无意义的地方。 HttpClient 实际上只是抽象了一堆在单元测试上下文中无关紧要的东西,然后调用消息处理程序(这是在这个线程中多次提出的一点)。 我还尝试为它创建一个包装器,这根本不值得实现它或传播实践所需的工作(即“哟,每个人都这样做而不是直接使用 HttpClient”)。 简单地关注处理程序要容易得多,因为它为您提供所需的一切,并且实际上由一个方法组成。

也就是说,创建了自己的 RestClient,但这解决了一个不同的问题,即提供了一个流畅的请求构建器,但即使该客户端也接受一个消息处理程序,该处理程序可用于单元测试或用于实现处理诸如处理程序链之类的自定义处理程序解决横切关注点(例如日志记录、身份验证、重试逻辑等),这是要走的路。 这并不特定于我的休息客户端,这只是设置处理程序的一个很好的用例。 由于这个原因,我实际上更喜欢 Windows 命名空间中的 HttpClient 接口,但我离题了。

但是,我认为接口处理程序仍然很有用,但它必须停在那里。 然后可以设置您的模拟框架以返回预先确定的 HttpResponseMessage 实例。

有趣的。 我发现(个人偏见?)我的帮助程序库在使用(假)具体消息处理程序时效果很好.. vs.. 一些低级别的接口东西。

不过,我仍然希望不必编写该库或使用它:)

我认为创建一个小型库来构建假货没有任何问题。 当我无聊无事可做时,我可能会这样做。 我所有的 http 东西都已经被抽象和测试过了,所以我现在还没有真正的用处。 为了单元测试的目的,我只是没有看到包装 HttpClient 的任何价值。 伪造处理程序是您真正需要的。 扩展功能是一个完全独立的话题。

当大部分代码库使用模拟接口进行测试时,其余代码库以相同方式测试会更加方便和一致。 所以我想看一个接口IHttpClient。 像 ADL SDK 中的 IFileSystemOperarions 或 ADF 管理 SDK 中的 IDataFactoryManagementClient 等。

我仍然认为您错过了重点,即 HttpClient 不需要被嘲笑,只需处理程序即可。 真正的问题是人们看待 HttpClient 的方式。 每当你想打电话给互联网时,这不是应该更新的随机类。 事实上,在整个应用程序中重用客户端是最佳实践——这很重要。 把这两件事分开,它更有意义。

另外,您的依赖类不应该关心 HttpClient,只关心它返回的数据——这些数据来自处理程序。 这样想:您是否打算用其他东西替换 HttpClient 实现? 可能……但不太可能。 你永远不需要改变客户端的工作方式,那么为什么还要抽象它呢? 消息处理程序是变量。 您想更改响应的处理方式,而不是客户端的操作。 甚至 WebAPI 中的管道也专注于处理程序(请参阅:委托处理程序)。 我说得越多,我就越开始认为 .Net 应该使客户端静态并为您管理它……但我的意思是……随便。

记住接口的用途。 它们不是用来测试的——这只是一种利用它们的聪明方法。 仅仅为此目的创建接口是荒谬的。 Microsoft 为您提供了解耦消息处理行为所需的内容,并且它非常适合测试。 实际上,HttpMesageHandler 是抽象的,所以我认为像 Moq 这样的大多数模拟框架仍然可以使用它。

@dasjestyr - 我也认为你可能错过了我讨论的重点。

事实上,我们(开发人员)需要 _learn_ 大量了解消息处理程序等 .. 来伪造响应,这是我对所有这些的 _main_ 观点。 与接口无关。 当然,在测试(因此是模拟)方面,我(个人)更喜欢 virtuals r 包装器的接口......但这些都是实现细节。

我希望这个史诗线程的主要要点是强调..在应用程序中使用 HttpClient 时,它是一个 PITA 来测试它。

“去学习 HttpClient 的管道,会带你到 HttpMessageHandler 等”的现状很糟糕。 对于许多其他库等,我们不需要这样做。

所以我希望能做些什么来缓解这个 PITA。

是的,PITA 是一种观点——我完全承认这一点。 有些人根本不认为它是 PITA,等等。

真正的问题是人们看待 HttpClient 的方式。 每当你想打电话给互联网时,这不是应该更新的随机类。

同意! 但直到最近,这还不为人所知——这可能导致缺乏文档或教学等。

另外,您的依赖类不应该关心 HttpClient,只关心它返回的数据

是的 - 同意。

它来自处理程序。

什么? 嗯? 哦——现在你要我打开引擎盖了解管道? ... 一遍又一遍地参考上面。

这样想:您是否打算用其他东西替换 HttpClient 实现?

不。

.. 等等 ..

现在,我并不是要拖钓等。所以请不要认为我总是一遍又一遍地回复和重复相同的东西,从而试图成为一个混蛋。

我已经使用我的助手库很长时间了,这只是使用自定义消息处理程序的一种美化方式。 伟大的! 它工作得很好。 我只是认为这可能会_all_暴露得更好......这就是我希望这个线程真正击中家的原因。

编辑:格式化。

(我只是注意到这个问题的标题中的语法错误,现在我无法看到它)

那是我真正的长期游戏:)

量子点

(其实——好尴尬😊)

编辑:我的一部分不想编辑它..怀旧....

(我只是注意到这个问题的标题中的语法错误,现在我无法看到它)

我知道! 相同的!

我们(开发人员)需要学习很多关于消息处理程序等的知识来伪造响应,这是我对所有这些的主要观点。 与接口无关。 当然,在测试(因此是模拟)方面,我(个人)更喜欢 virtuals r 包装器的接口......但这些都是实现细节。

什么?

你甚至看过 HttpMessageHandler 吗? 它是一个抽象类,实际上是一个接受 HttpRequestMessage 并返回 HttpResponseMessage 的单一方法——用于拦截与所有低级传输垃圾分开的行为,这正是您在单元测试中想要的。 要伪造它,只需实施它。 传入的消息是您发送 HttpClient 的消息,而输出的响应取决于您。 例如,如果我想知道我的代码是否正确处理了 json 正文,那么只需让响应返回您期望的 json 正文。 如果您想查看它是否正确处理 404,则让它返回 404。没有比这更基本的了。 要使用处理程序,只需将其作为 HttpClient 的 ctor 参数发送。 您无需拔出任何电线或了解任何东西的内部运作。

我希望这个史诗线程的主要要点是强调..在应用程序中使用 HttpClient 时,它是一个 PITA 来测试它。

许多人指出的主要要点是你做错了(这就是为什么它是 PITA,我直接但尊重地这么说)并且要求 HttpClient 被接口用于测试目的类似于创建功能补偿错误而不是解决根本问题,在这种情况下,您的操作级别错误。

我认为 windows 命名空间中的 HttpClient 实际上确实将处理程序的概念分成了filter ,但它做的事情完全相同。 我认为该实现中的处理程序/过滤器实际上是接口的,尽管这有点像我之前建议的。

什么?
你甚至看过 HttpMessageHandler 吗?

最初没有,因为:

var payloadData = await _httpClient.GetStringAsync("https://......");

意思是, HttpClient上的公开方法非常好,让事情_really_容易:) 耶! 快速,简单,有效,赢!

好的 - 现在让我们在测试中使用它......现在我们需要花时间学习下面发生的事情......这导致我们学习HttpMessageHandler等。

我再说一遍 => 这个关于HttpClient管道的额外学习(即HMH等)是一个 PITA。 一旦你_完成了学习_ ..是的,我就知道如何使用它等等......即使我也不喜欢继续使用它但需要,以便模拟对第 3 方端点的远程调用。 当然,这是自以为是。 我们只能同意不同意。 所以请 - 我知道HMH以及如何使用它。

许多人指出的主要要点是你做错了

是的——人们不同意我的观点。 没有问题。 还有——人们也同意我的看法。 再次,没有问题。

并且_要求_为了测试目的而连接 HttpClient 类似于创建功能来补偿错误,而不是解决根本问题,在这种情况下,您在错误的级别上操作。

好的 - 我在这里恭敬地不同意你的看法(没关系)。 再次,意见不一。

但是很严肃。 这个话题已经持续了这么久。 我现在真的继续前进了。 我说我的一块,有人说YES! 有人说不! 什么都没有改变,我只是……接受了现状并接受了一切。

对不起,你只是不接受我的思路。 我不是一个坏人,尽量不要听起来粗鲁或刻薄。 这只是我表达了我在发展过程中的感受以及我相信其他人的感受。 就这样。

我一点也不生气。 我最初以同样的观点跳上这个线程,认为客户端应该是可模拟的,但后来关于 HttpClient 是什么提出了一些非常好的观点,所以我坐下来认真思考它,然后尝试自己挑战它,最终我得出了相同的结论,即 HttpClient 是错误的模拟对象,尝试这样做是徒劳的,会导致工作量远远超过其价值。 接受这一点让生活变得更轻松。 所以我也继续前进。 我只是希望其他人最终会给自己一个休息时间并接受它的本来面目。

作为旁白:

最初没有,因为:

var payloadData = await _httpClient.GetStringAsync("https://......");

意思是,HttpClient 上公开的方法非常好,让事情变得非常简单:) 耶! 快速,>简单,有效,赢了!

我认为就 SOLID 而言,这个接口无论如何都是不合适的。 IMO 客户端、请求、响应是 3 个不同的职责。 我可以欣赏客户端上的便利方法,但它通过将请求和响应与客户端结合来促进紧密耦合,但这是个人喜好。 我对 HttpResponseMessage 进行了一些扩展,它们完成了同样的事情,但负责读取响应消息的响应。 以我的经验,对于大型项目,“简单”从来都不是“简单”,而且几乎总是以 BBOM 结尾。 然而,这是一个完全不同的讨论:)

现在我们有一个专门用于设计讨论的新 repo,请在https://github.com/dotnet/designs/issues/9中继续讨论或在https://github.com/dotnet/designs中打开一个新问题

谢谢。

也许它可以被视为仅用于测试目的的解决方案:

公共类 GeoLocationServiceForTest : GeoLocationService, IGeoLocationService
{
公共 GeoLocationServiceForTest(something: something, HttpClient httpClient) : base(something)
{
base._httpClient = httpClient;
}
}

我最终使用了@richardszalayMockHttp
谢谢,理查德,你拯救了我的一天!

好的,我可以忍受无接口的HttpClient 。 但是被迫实现一个继承HttpMessageHandler的类,因为SendAsync是受保护的,这很糟糕。

我使用 NSubstitute,但这不起作用:

var httpMessageHandler = 
    Substitute.For<HttpMessageHandler>(); // cannot be mocked, not virtual nor interfaced
httpMessageHandler.SendAsync(message, cancellationToken)
    .Return(whatever); // doesn't compile, it's protected
var httpClient = new HttpClient(httpMessageHandler)

真是令人失望。

此页面是否有帮助?
0 / 5 - 0 等级