如何测试依赖于复杂API(例如Amazon S3)的代码?


13

我正在努力测试一种将文档上传到Amazon S3的方法,但是我认为这个问题适用于任何非平凡的API /外部依赖。我只提出了三种潜在的解决方案,但似乎没有一个令人满意的:

  1. 请运行代码,实际上传文档,使用AWS的API检查文档是否已上传,然后在测试结束时将其删除。这将使测试非常缓慢,每次运行测试都将花费金钱,并且永远不会返回相同的结果。

  2. 模拟S3。这是超级毛茸茸的,因为我不知道该对象的内部,并且感觉太错了,因为它太复杂了。

  3. 只要确保使用正确的参数调用MyObject.upload(),并相信我正确使用了S3对象。这使我感到困扰,因为无法确定仅从测试中就正确使用了S3 API。

我检查了亚马逊如何测试他们自己的SDK,以及它们是否在模拟一切。他们有一个200行的助手来进行模拟。我觉得这样做不可行。

我该如何解决?


这不是一个答案,但是在实践中,我们确实使用了您公开的三种方法。
jlhonora 2015年

Answers:


28

这里我们要看两个问题。

首先,您似乎正在从单元测试的角度查看所有测试。单元测试非常有价值,但不是唯一的测试类型。实际上,测试可以分为几个不同的层,从非常快的单元测试到不太快的集成测试,再到更慢的验收测试。(甚至可以分解出更多的层,例如功能测试。)

第二个是您将对第三方代码的调用与您的业务逻辑混合在一起,带来了测试挑战,并可能使您的代码更加脆弱。

单元测试应该快速并且应该经常运行。模拟依赖关系有助于保持这些测试的快速运行,但是如果依赖关系发生变化而模拟保持不变,则可能会在覆盖率方面造成漏洞。测试仍然可以绿色运行时,您的代码可能会被破坏。如果依赖项的界面发生更改,某些模拟库将提醒您,而其他库则不会。

另一方面,集成测试旨在测试组件之间的交互,包括第三方库。在此测试级别上不应使用模拟,因为我们想了解实际对象如何相互作用。因为我们使用的是真实对象,所以这些测试会变慢,并且我们将不会像单元测试那样频繁地运行它们。

验收测试的水平更高,测试是否满足软件要求。这些测试针对将要部署的整个完整系统进行。再一次,不应使用任何嘲笑。

人们发现的关于嘲讽的一项有价值的准则是不要嘲笑你不拥有的类型。亚马逊拥有S3的API,因此他们可以确保其下的API不会发生变化。另一方面,您没有这些保证。因此,如果您在测试中模拟了S3 API,则它可能会更改并破坏您的代码,而所有测试均显示为绿色。那么,我们如何对使用第三方库的测试代码进行单元化?

好吧,我们没有。如果遵循该准则,则无法模拟不拥有的对象。但是……如果我们拥有我们的直接依赖关系,我们可以将它们模拟掉。但是如何?我们为S3 API创建了自己的包装器。我们可以使其看起来很像S3 API,或者可以使其更紧密地适应我们的需求(首选)。我们甚至可以将其抽象一些,用a PersistenceService而不是a表示AmazonS3BucketPersistenceService将会是一个接口,其中包含诸如#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编写的代码。


问题是如何对所公开的API运行集成或端到端测试。出于明显的原因,您无法模拟PersistenceService。我误解了一些东西,或者在应用程序API和AWS API之间添加了另一层,无非是让您更轻松地进行单元测试
Yerken

@Yerken在思考的过程中,我很确定我可以为该问题填写一个较长的答案。这甚至对您来说是值得的,因为您可能获得的不仅仅是我的答复。
cbojar

4

您需要同时做。

运行,上传和删除是一项集成测试。它与外部系统连接,因此可以预期运行缓慢。它可能不应该是您在本地进行的每个构建的一部分,而应该是CI构建或夜间构建的一部分。这抵消了这些测试的缓慢性,仍然提供了自动测试的价值。

您还需要运行更快的单元测试。由于通常不过度依赖外部系统是很明智的(因此您可以交换实现或切换),因此您可能应该尝试在S3上编写一个简单的接口,以对其进行编码。在单元测试中模拟该接口,以便您可以快速运行单元测试。

第一个测试检查您的代码是否确实适用于S3,第二个测试检查您的代码正确调用了与S3对话的代码。


2

我要说的是,这取决于您使用API​​的复杂性

  1. 您肯定至少需要进行一些测试,这些测试实际上会调用S3 API并确认它从头到尾都有效。

  2. 您还肯定需要进行其他未实际调用API的测试,因此您可以在无需始终调用API的情况下充分测试自己的软件。

剩下的问题是:您是否需要模拟API?

我认为这取决于您如何处理。如果您仅执行一个或两个简单的操作,那么我认为您无需费心处理模型的所有麻烦。只要检查一下我对这些功能的使用并进行一些实时测试,我就会感到满意。

但是,如果您使用它更为复杂,并且具有可能影响结果的不同方案和变量,则可能需要对其进行模拟以进行更彻底的测试。


1

除了前面的答案之外,主要的问题是您是否(以及如何)模拟用于测试的S3 API。

无需手动模拟单个S3响应,您可以利用一些非常复杂的现有模拟框架。例如moto提供的功能与实际的S3 API非常相似。

您还可以查看LocalStack,该框架结合了现有工具并提供了功能全面的本地云环境(包括S3),可促进集成测试。

尽管其中一些工具是用其他语言(Python)编写的,但是应该很容易在外部流程中从Java / JUnit等测试中扩展测试环境。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.