我最近一直在阅读有关单元测试的很多内容。我目前正在阅读 Roy Osherove 的单元测试艺术。但是书中没有正确解决一个问题:你如何确保你的存根的行为完全像真实的东西一样?#34;呢?
例如,我正在尝试创建一个ImageTagger应用程序。在那里,我有一个ImageScanner类,它可以找到文件夹中的所有图像。我想测试的方法具有以下签名:IEnumerable<Image> FindAllImages(string folder)
。如果该文件夹中没有任何图像,则该方法应返回null。
该方法本身会发出对System.IO.Directory.GetFiles(..)
的调用,以便查找该文件夹中的所有图像。
现在,我想编写一个测试,确保FindAllImages(..)
在文件夹为空时返回null。正如 Osherove 在他的书中所写,我应该提取一个具有单一方法GetFiles(..)
的接口ID目录。此接口注入我的ImageScanner类。实际实现只调用System.IO.Directory.GetFiles(..)
但是接口允许我创建可以模拟Directory.GetFiles()
行为的存根。
如果没有任何文件,Directory.GetFiles会返回一个空数组。所以我的存根看起来像这样:
class EmptyFolderStub : IDirectory
{
string[] GetFiles(string path)
{
return new string[]{};
}
}
我可以将EmptyFolderStub注入我的ImageScanner类并测试它是否返回null。
现在,如果我认为有更好的库来搜索文件怎么办?但是如果没有要找到的文件,它的GetFiles(..)
方法会抛出异常或返回null。我的测试仍然通过,因为存根模拟旧的GetFiles(..)
行为。但是,生产代码将失败,因为它没有准备好处理新库中的null或异常。
当然,您可以说通过提取接口ID目录,还存在一个合同,该合同保证IDirectory.GetFiles(..)
方法应该返回一个空数组。所以从技术上讲,我必须测试实际的实现是否满足该合同。但显然,除了集成测试之外,没有办法判断该方法是否真的以这种方式运行。当然,我可以阅读API规范并确保它返回一个空数组,但不要认为这是单元测试的重点。
我该如何克服这个问题?这甚至可以用于单元测试,还是我必须依靠集成测试来捕获像这样的边缘情况?我觉得我正在测试一些我无法控制的东西。但我至少期望在我引入新的不兼容库时会有一个测试中断。
答案 0 :(得分:1)
当然,您可以通过提取接口ID目录来说明 还有一份保证合同的合同 IDirectory.GetFiles(..)方法应该返回一个空数组。 ...当然,我可以阅读API规范并确保这一点 它返回一个空数组,但不要认为这是单位的重点 测试
是的,我会这么说。您没有更改签名,但您正在更改合同。接口是合同,而不仅仅是签名。这就是为什么这是单元测试的重点 - 如果合同确实发挥作用,那么这个单元就会发挥作用。
如果你想要额外的安心,你可以针对IDirectory编写单元测试,在你的例子中,那些会破坏(期望空字符串数组)。如果您更改实现(或当前实现的新版本会破坏它),这将提醒您合同更改。
答案 1 :(得分:1)
在单元测试中,识别SUT(被测系统)非常重要。一旦识别出来,就应该用stub来替换它们的依赖关系。 为什么?因为我们想要假装我们生活在一个完美的无bug合作者世界中,在这种情况下我们想要检查SUT的行为方式。
你的SUT肯定是FindAllImages
。坚持这一点,以避免迷路。所有存根实际上都是依赖项(协作者)的替代品,它们应该可以完美地工作而不会出现任何故障(这就是它们存在的原因)。存根不能通过测试。存根是想象中的完美物体。
注意:这种配置有重要意义。它有一个哲学背后:
*如果测试通过,假设所有依赖项都正常工作(通过使用存根或实际对象),则保证SUT按预期运行。 换句话说,如果依赖关系起作用,则SUT起作用,因此绿色测试在任何环境中保持其含义:如果A - >然后B
但它没有说依赖性是否失败然后SUT测试应该通过或不通过。恕我直言,任何进一步的逻辑解释都是误导性的。 如果不是A - >然后 ??? (我们不能说什么)
总结: 失败的实际依赖性以及SUT应该如何反应是另一种测试场景。您可以设计一个抛出异常的存根,并检查SUT的预期 行为。