【问题标题】:Excluding code from test coverage从测试覆盖中排除代码
【发布时间】:2023-03-16 13:29:01
【问题描述】:

我尽可能使用 TDD:

  • 我模拟了我的界面
  • 我使用 IOC,因此可以注入我的模拟对象
  • 我确保我的测试能够运行并且覆盖范围增加,我很高兴。

那么……

  • 我创建派生类来实际做一些事情,例如访问数据库或写入消息队列等。

这是代码覆盖率降低的地方 - 我感到难过。

然后,我在这些具体的类上大肆传播[CoverageExclude],覆盖率再次上升。

但是,我没有感到悲伤,而是感到肮脏。尽管无法对具体类进行单元测试,但我还是觉得自己在作弊。

我有兴趣了解您的项目是如何组织的,即您如何实际安排可以测试的代码与无法测试的代码。

我认为也许一个不错的解决方案是将不可测试的具体类型分离到它们自己的程序集中,然后禁止在包含可测试代码的程序集中使用[CoverageExclude]。当在可测试程序集中错误地找到此属性时,这也可以更轻松地创建 NDepend 规则以使构建失败。


编辑:这个问题的本质涉及这样一个事实,即您可以测试使用模拟接口的东西,但您不能(或不应该!)对作为这些接口的真实实现的对象进行单元测试.这是一个例子:

public void ApplyPatchAndReboot( )
{ 
    _patcher.ApplyPatch( ) ;
    _rebooter.Reboot( ) ;
}

patcher 和 rebooter 被注入到构造函数中:

public SystemUpdater(IApplyPatches patcher, IRebootTheSystem rebooter)...

单元测试如下:

public void should_reboot_the_system( )
{
    ... new SystemUpdater(mockedPatcher, mockedRebooter);
    update.ApplyPatchAndReboot( );
}

这很好 - 我的 UNIT-TEST 覆盖率为 100%。我现在写:

public class ReallyRebootTheSystemForReal : IRebootTheSystem
{
    ... call some API to really (REALLY!) reboot
}

我的 UNIT-TEST 覆盖率下降,无法对新课程进行 UNIT-TEST。当然,我会添加一个功能测试并在我有 20 分钟的空闲时间时运行它(!)。

所以,我想我的问题归结为一个事实,即拥有接近 100% 的 UNIT-TEST 覆盖率是件好事。换句话说,能够对接近 100% 的系统行为进行单元测试真是太好了。在上面的例子中,修补程序的行为应该重新启动机器。我们肯定可以verify。 ReallyRebootTheSytemForReal 类型不仅仅是行为——它有副作用,这意味着它不能进行单元测试。由于它不能进行单元测试,因此会影响测试覆盖率。所以,

  • 这些东西是否会降低单元测试覆盖率?
  • 是否应该将它们隔离到人们期望 0% 的 UNIT-TEST 覆盖率的自己的程序集中?
  • 这样的具体类型是否应该如此小(在循环复杂度中)以至于单元测试(或其他)是多余的

【问题讨论】:

  • “无法测试”是什么意思??障碍是什么?
  • 诸如触及不可触及区域的接口的特定实现之类的东西,例如消息队列、数据库、文件系统等。例如,可以模拟 IWriteToAQueue 之类的接口,并且所有期望 IWriteToAQueue 的位都可以使用模拟进行测试。但是 - 仅写入 MSMQ 的名为“WriteToMsmq”的具体类型无法进行单元测试。

标签: unit-testing tdd inversion-of-control mocking


【解决方案1】:

你在正确的轨道上。您可能可以测试的一些具体实现,例如数据访问组件。对关系数据库进行自动化测试肯定是可能的,但也应该将其分解到它自己的库中(使用相应的单元测试库)。

由于您已经在使用依赖注入,因此将这样的依赖组合回您的实际应用程序应该是小菜一碟。

另一方面,也会有一些具体的依赖关系本质上是不可测试的(或者像 Fowler 曾经开玩笑说的那样是不可测试的)。此类实现应尽可能保持精简。通常,可以设计这样的 Dependency 暴露的 API,使所有逻辑都发生在消费者中,并且实际实现的复杂性非常低。

实现这种具体的依赖是一个明确的设计决策,当您做出该决定时,您同时决定不应对此类库进行单元测试,因此不应测量代码覆盖率。

这样的库称为 Humble Object。它(和许多其他模式)在优秀的xUnit Test Patterns 中有描述。

根据经验,如果代码的圈复杂度为 1,我接受该代码未经测试。在这种情况下,它或多或少纯粹是声明性的。务实地说,不可测试的组件只要具有低圈复杂度,就可以正常使用。 “低”到底有多低,你必须自己决定。

无论如何,[CoverageExclude] 对我来说似乎是一种气味(在阅读您的问题之前我什至不知道它存在)。

【讨论】:

  • 感谢马克的评论。我一定会阅读测试模式书。我还要看看CC;我怀疑它非常低。为了清楚起见,当我使用“测试”这个词时,我的意思是纯粹的“单元测试”,即测试不同类型的行为。 CoverageExclude 属性被 NCover 识别。
【解决方案2】:

我不明白你的具体类是如何不可测试的。这对我来说很难闻。

如果您有一个正在写入消息队列的具体类,您应该能够向它传递一个模拟队列,并测试它的所有方法就好了。如果您的课程要访问数据库,那么您应该可以将模拟数据库交给它。

在某些情况下可能会导致无法测试的代码,我不会否认 - 但这应该是例外,而不是规则。你所有的具体的类工作者对象?有点不对劲。

【讨论】:

  • 如果您的资源访问组件与旧系统或公共 Web 服务通信,则很难测试与此类系统通信的实际实现。
  • womp,一些具体的类是不可测试的,因为它们涉及数据库、消息队列等。你说如果我有一个写入消息队列的具体类,那么我可以给它一个模拟队列。这是完全正确的,但最终会有一些软件可以物理地写入队列:物理上接触队列的最终具体实现。即使使用它的所有东西(或更准确地说,它的界面)都已经过测试,这也是不可测试的。
  • 如果你注入你的队列,那么你的类对模拟的行为与对真实队列的行为没有什么不同。它在物理上接触队列,它在物理上写入队列。你可以测试一下。那里没有无法测试的代码行。
  • 当然,我可以测试使用队列编写接口的东西,而且我可以——这些都包含在单元测试中。然后我编写了队列写入组件的代码。它初始化队列并写入它。这不能在单元测试的正常意义上进行单元测试(不要触及外部系统等)当你说没有一行代码无法测试时:这是真的,但对于'单元测试'。尝试编写测试“should_reboot_machine”! :)
  • 对于真正的“单元测试”没有明确的界限。数据访问组件 (DAC) 和与之通信的数据库可以被视为一个单元。你仍然可以测试它。您是否坚持将这样的自动化测试称为“单元测试”或“集成测试”并不重要。重要的部分是编写这样的测试是否有价值。对于 DAC,通常有。另一方面,肯定有一些东西是你无法测试的,比如你刚刚给出的重启示例。只要我们将这些实现为 Humble Objects,我们就应该是好的。
【解决方案3】:

扩展 womps 答案:我怀疑您考虑的更多是“不可测试”,而不是实际情况。在不同时测试任何依赖项的情况下,在严格的“一次一个单元”单元测试中无法测试?当然。但它应该很容易通过更慢和更不频繁运行的集成样式测试来实现。

您提到访问数据库并将消息写入队列。正如 womp 所提到的,您可以在单元测试期间为他们提供模拟数据库和模拟队列,并在集成测试中测试实际的具体行为。就我个人而言,我认为直接将具体实现作为单元测试进行测试也没有任何问题,至少当它们不是远程(或遗留)时。当然,它们的运行速度会慢一些,但是,嘿,至少它们被自动化测试覆盖了。

您是否会将消息写入队列但没有实际测试消息是否写入实际物理/逻辑队列的系统投入生产?我不会。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-10-09
    • 2020-06-12
    • 2015-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-02
    • 1970-01-01
    相关资源
    最近更新 更多