这里我们要看两个问题。
首先,您似乎正在从单元测试的角度查看所有测试。单元测试非常有价值,但不是唯一的测试类型。实际上,测试可以分为几个不同的层,从非常快的单元测试到不太快的集成测试,再到更慢的验收测试。(甚至可以分解出更多的层,例如功能测试。)
第二个是您将对第三方代码的调用与您的业务逻辑混合在一起,带来了测试挑战,并可能使您的代码更加脆弱。
单元测试应该快速并且应该经常运行。模拟依赖关系有助于保持这些测试的快速运行,但是如果依赖关系发生变化而模拟保持不变,则可能会在覆盖率方面造成漏洞。测试仍然可以绿色运行时,您的代码可能会被破坏。如果依赖项的界面发生更改,某些模拟库将提醒您,而其他库则不会。
另一方面,集成测试旨在测试组件之间的交互,包括第三方库。在此测试级别上不应使用模拟,因为我们想了解实际对象如何相互作用。因为我们使用的是真实对象,所以这些测试会变慢,并且我们将不会像单元测试那样频繁地运行它们。
验收测试的水平更高,测试是否满足软件要求。这些测试针对将要部署的整个完整系统进行。再一次,不应使用任何嘲笑。
人们发现的关于嘲讽的一项有价值的准则是不要嘲笑你不拥有的类型。亚马逊拥有S3的API,因此他们可以确保其下的API不会发生变化。另一方面,您没有这些保证。因此,如果您在测试中模拟了S3 API,则它可能会更改并破坏您的代码,而所有测试均显示为绿色。那么,我们如何对使用第三方库的测试代码进行单元化?
好吧,我们没有。如果遵循该准则,则无法模拟不拥有的对象。但是……如果我们拥有我们的直接依赖关系,我们可以将它们模拟掉。但是如何?我们为S3 API创建了自己的包装器。我们可以使其看起来很像S3 API,或者可以使其更紧密地适应我们的需求(首选)。我们甚至可以将其抽象一些,用a PersistenceService
而不是a表示AmazonS3Bucket
。PersistenceService
将会是一个接口,其中包含诸如#save(Thing)
和#fetch(ThingId)
的方法,我们可能希望看到的方法类型(这些是示例,实际上您可能需要其他方法)。现在,我们可以PersistenceService
围绕S3 API(例如S3PersistenceService
)实现一个,将其封装在调用代码之外。
现在到调用S3 API的代码。我们需要将这些调用替换为对PersistenceService
对象的调用。我们使用依赖注入将我们传递PersistenceService
给对象。重要的是不要索要一个S3PersistenceService
,而要索要一个PersistenceService
。这使我们可以在测试期间交换实现。
现在,用于直接使用S3 API的所有代码现在都使用了我们的PersistenceService
,并且我们S3PersistenceService
现在进行了对S3 API的所有调用。在我们的测试中,PersistenceService
由于我们拥有它,因此我们可以对其进行模拟,并使用该模拟来确保我们的代码进行了正确的调用。但是现在剩下如何测试了S3PersistenceService
。它具有与以前相同的问题:我们不能在不调用外部服务的情况下对其进行单元测试。所以...我们不对它进行单元测试。我们可以模拟出S3 API的依赖关系,但这将使我们几乎没有信心。相反,我们必须在更高级别上对其进行测试:集成测试。
说我们不应该对代码的一部分进行单元测试,这听起来有些令人不安,但是让我们看看我们完成了什么。我们在无法进行单元测试的地方到处都是一堆代码,现在可以通过进行单元测试了PersistenceService
。我们将第三方库的混乱局限在单个实现类中。该类应提供使用API所必需的功能,但不附加任何外部业务逻辑。因此,一旦编写,它应该非常稳定并且不应有太大变化。我们可以依赖较慢的测试,因为它们是稳定的,因此我们不经常运行这些测试。
下一步是为编写集成测试S3PersistenceService
。这些应按名称或文件夹分开,以便我们可以与快速单元测试分开运行它们。如果代码具有足够的信息量,集成测试通常可以使用与单元测试相同的测试框架,因此我们不需要学习新工具。集成测试的实际代码就是您为Option 1编写的代码。