单元测试:正确包装库调用

本文关键字:调用 包装 单元测试 | 更新日期: 2023-09-27 18:17:22

我最近读了很多关于单元测试的文章。我目前正在阅读Roy Osherove的《The Art of Unit Testing》。但有一个问题在书中没有得到适当的解决:如何确保存根的行为与"真实的东西"完全一样?

例如,我正在尝试创建一个ImageTagger应用程序。在那里,我有一个类ImageScanner,它的工作是找到文件夹内的所有图像。我要测试的方法有以下签名:IEnumerable<Image> FindAllImages(string folder)。如果文件夹中没有任何图像,该方法应该返回null。方法本身会调用System.IO.Directory.GetFiles(..)来查找文件夹内的所有图像。

现在,我想写一个测试,确保FindAllImages(..)返回null,如果文件夹是空的。正如Osherove在他的书中所写,我应该提取一个接口IDirectory,它有一个单一的方法GetFiles(..)。这个接口被注入到我的imagesscanner类中。实际的实现只是调用System.IO.Directory.GetFiles(..)接口,但允许我创建可以模拟Directory.GetFiles()行为的存根。

目录。如果没有文件,GetFiles返回一个空数组。所以我的存根是这样的:

   class EmptyFolderStub : IDirectory
   {
     string[] GetFiles(string path)
     {
       return new string[]{};
     }
   }

我可以注入EmptyFolderStub到我的ImageScanner类和测试它是否返回null。现在,如果我决定有一个更好的库来搜索文件呢?但是它的GetFiles(..)方法抛出一个异常,或者如果没有找到文件就返回null。我的测试仍然通过,因为存根模拟了旧的GetFiles(..)行为。但是,生产代码将失败,因为它没有准备好处理来自新库的null或异常。

当然,你可以说,通过提取Interface IDirectory,也存在一个契约,该契约保证IDirectory.GetFiles(..)方法应该返回一个空数组。从技术上讲,我需要测试实际实现是否满足契约。但是显然,除了集成测试之外,没有办法判断方法是否真的以这种方式运行。当然,我可以阅读API规范并确保它返回一个空数组,但不要认为这是单元测试的重点。

我怎样才能克服这个问题?这是可能的单元测试,还是我必须依靠集成测试来捕捉像这样的边缘情况?我感觉我在测试一些我无法控制的东西。但是我至少希望当我引入一个新的不兼容的库时,有一个测试会中断。

单元测试:正确包装库调用

在单元测试中,识别SUT(被测系统)非常重要。一旦确定了它,就应该用存根替换它的依赖项。为什么?因为我们想假装我们生活在一个没有bug的合作者的完美世界里,在这种情况下,我们想检查SUT的行为。

你的SUT肯定是FindAllImages。坚持这样做,以免迷路。所有存根实际上都是依赖项(协作器)的替代品,它们应该完美地工作而不会出现任何故障(这就是它们存在的原因)。存根不能通过测试。存根是想象中的完美物体。注意:此配置具有重要意义。它背后有一个哲学:

*如果一个测试通过了,假设它的所有依赖项都工作良好(通过使用存根或实际对象),那么SUT就可以保证按照预期的方式运行。换句话说,如果依赖关系有效,则SUT有效,因此绿色测试在任何环境中都保留其意义:如果A ->则B

但是它并没有说如果依赖关系失败,那么SUT测试应该通过或不通过之类的。恕我直言,任何进一步的逻辑解释都是误导性的。如果不是A ->那么??(我们不能说什么)

在简介:

失败的实际依赖关系以及SUT应该如何反应是另一个测试场景。您可以设计一个存根,它抛出异常并检查SUT的预期值行为。

当然,您可以通过提取接口IDirectory来这么说还有一份合同保证方法IDirectory.GetFiles(..)应该返回一个空数组。…当然,我可以阅读API规范并确保它返回一个空数组,但不要认为这是单位的重点测试。

是的,我想这么说。你没改签名,但你在改合同。接口是一个契约,而不仅仅是一个签名。这就是为什么这是单元测试的重点——如果合同完成了它的部分,这个单元就完成了它的部分。

如果您想要额外的安心,您可以针对IDirectory编写单元测试,并且在您的示例中,这些测试将中断(期望空字符串数组)。如果您更改了实现(或者当前实现的新版本破坏了它),这将提醒您契约更改。