我尽可能使用 TDD:
然后...
这就是代码覆盖率下降的地方——我感到难过。
但后来,我大肆传播
[CoverageExclude]
在这些具体的类和覆盖率再次上升。但后来我没有感到悲伤,而是感到肮脏。尽管无法对具体类进行单元测试,但我还是觉得自己在作弊。
我有兴趣了解您的项目是如何组织的,即您如何在物理上安排可以针对无法测试的代码进行测试的代码。
我在想也许一个不错的解决方案是将不可测试的具体类型分离到它们自己的程序集中,然后禁止使用
[CoverageExclude]
在包含可测试代码的程序集中。当在可测试的程序集中错误地找到此属性时,这还可以更轻松地创建 NDepend 规则以使构建失败。编辑:这个问题的本质涉及这样一个事实,即您可以测试使用模拟接口(interface)的东西,但您不能(或不应该!)对作为这些接口(interface)的真实实现的对象进行单元测试。这是一个例子:
public void ApplyPatchAndReboot( )
{
_patcher.ApplyPatch( ) ;
_rebooter.Reboot( ) ;
}
patcher 和 rebooter 被注入(inject)到构造函数中:
public SystemUpdater(IApplyPatches patcher, IRebootTheSystem rebooter)...
单元测试如下所示:
public void should_reboot_the_system( )
{
... new SystemUpdater(mockedPatcher, mockedRebooter);
update.ApplyPatchAndReboot( );
}
这很好用——我的单元测试覆盖率为 100%。我现在写:
public class ReallyRebootTheSystemForReal : IRebootTheSystem
{
... call some API to really (REALLY!) reboot
}
我的 UNIT-TEST 覆盖率下降了,无法对新类(class)进行 UNIT-TEST 测试。当然,我会添加一个功能测试并在我有 20 分钟的空闲时间时运行它(!)。
所以,我想我的问题归结为这样一个事实,即拥有接近 100% 的 UNIT-TEST 覆盖率是件好事。换句话说,能够对接近 100% 的系统行为进行单元测试真是太好了。在上面的例子中,修补程序的行为应该重新启动机器。这个我们可以
verify
当然。 ReallyRebootTheSytemForReal
类型不仅仅是行为 - 它有副作用,这意味着它不能进行单元测试。由于它不能进行单元测试,因此会影响测试覆盖率。所以,最佳答案
你在正确的轨道上。您可能可以测试一些具体的实现,例如数据访问组件。对关系数据库进行自动化测试肯定是可能的,但也应该将其分解到它自己的库中(使用相应的单元测试库)。
由于您已经在使用依赖注入(inject),因此将这样的依赖组合回您的实际应用程序应该是小菜一碟。
另一方面,也将存在本质上不可测试的具体依赖项(或可取消测试,正如 Fowler 曾经开玩笑说的那样)。此类实现应尽可能保持精简。通常,可以设计这样一种依赖关系公开的 API,使所有逻辑都发生在消费者中,并且实际实现的复杂性非常低。
实现这种具体的依赖关系是一个明确的设计决策,当你做出这个决定时,你同时决定不应该对这样的库进行单元测试,因此不应该测量代码覆盖率。
这样的库称为 Humble Object。它(和许多其他模式)在优秀的 xUnit Test Patterns 中有描述。 .
根据经验,如果代码的循环复杂度为 1,我接受未经测试的代码。在这种情况下,它或多或少是纯粹的声明性代码。务实地说,不可测试的组件只要具有较低的循环复杂度,就可以正常运行。你必须自己决定“低”多低。
无论如何, [CoverageExclude] 对我来说似乎是一种气味(在我阅读您的问题之前,我什至不知道它存在)。
关于unit-testing - 从测试覆盖中排除代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1070136/