作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
卢卡Blažecki
验证专家 在工程

Luka (MCS)是一个专注于可扩展后端解决方案的团队领导者. 他精通节点.js、SQL和NoSQL数据库以及AWS.

专业知识

以前在

infobip
分享

集成测试不应该是可怕的事情. 它们是对应用程序进行全面测试的重要组成部分.

当谈到测试时, 我们通常认为单元测试是对一小块代码进行隔离测试. 然而, 您的应用程序比这一小块代码更大,并且应用程序的几乎没有任何部分是单独工作的. 这就是集成测试证明其重要性的地方. 集成测试可以弥补单元测试的不足, 它们在单元测试和端到端测试之间架起了桥梁.

您知道您需要编写集成测试,那么为什么不这样做呢?

在本文中, 您将学习如何使用基于api的应用程序中的示例编写可读和可组合的集成测试.

而我们将使用JavaScript/节点.js 对于本文中的所有代码示例, 讨论的大多数想法都可以很容易地适用于任何平台上的集成测试.

单元测试与集成测试:两者都需要

单元测试关注于一个特定的代码单元. 通常,这是一个特定的方法或一个更大组件的函数.

这些测试是单独完成的,其中所有外部依赖项通常都是存根或模拟的.

换句话说, 依赖关系被预先编程的行为所取代, 确保测试的结果仅由被测试单元的正确性决定.

您可以了解有关单元测试的更多信息 在这里.

单元测试用于维护具有良好设计的高质量代码. 它们还使我们能够轻松地覆盖极端情况.

然而,缺点是单元测试不能覆盖组件之间的交互. 这就是集成测试变得有用的地方.

集成测试

如果单元测试是通过孤立地测试最小的代码单元来定义的, 那么集成测试正好相反.

集成测试用于测试多个, 更大的交互单元(组件), 有时甚至可以跨越多个系统.

集成测试的目的是在各个组件之间的连接和依赖关系中发现错误, 如:

  • 传递无效或顺序不正确的参数
  • 数据库模式损坏
  • 无效的缓存集成
  • 业务逻辑中的缺陷或数据流中的错误(因为现在从更广泛的角度进行测试).

如果我们测试的组件没有任何复杂的逻辑(例如.g. 最小值组件 圈复杂度),集成测试将比单元测试重要得多.

在这种情况下,单元测试将主要用于执行良好的代码设计.

单元测试有助于确保函数被正确地编写, 集成测试有助于确保系统作为一个整体正常工作. 因此,单元测试和集成测试都有各自的互补目的, 两者对于全面的测试方法都是必不可少的.

单元测试和集成测试就像一个硬币的两面. 没有两者,硬币就无效.

因此,在完成集成测试和单元测试之前,测试是不完整的.

设置集成测试套件

而为单元测试设置一个测试套件是非常简单的, 为集成测试设置测试套件通常更具挑战性.

例如, 集成测试中的组件可以具有项目外部的依赖项, 像数据库, 文件系统, 电子邮件提供商, 对外支付服务, 等等.......

偶尔, 集成测试需要使用这些外部服务和组件, 有时它们可以被存根.

当需要它们时,可能会带来一些挑战.

  • 脆弱的测试执行: 外部服务可能不可用、返回无效响应或处于无效状态. 在某些情况下,这可能导致假阳性,其他时候可能导致假阴性.
  • 缓慢的执行: 准备和连接到外部服务可能很慢. 通常,测试是在外部服务器上运行的 CI.
  • 复杂的测试设置: 外部服务需要处于测试所需的状态. 例如,数据库应该预先加载必要的测试数据,等等.

编写集成测试时遵循的说明

集成测试不像单元测试那样有严格的规则. 尽管如此,在编写集成测试时还是有一些通用的方向可以遵循.

可重复的测试

测试顺序或依赖关系不应该改变测试结果. 多次运行相同的测试应该总是返回相同的结果. 如果测试使用Internet连接到第三方服务,则很难实现这一点. 然而,这个问题可以通过stub和mock来解决.

对于您有更多控制的外部依赖项, 在集成测试之前和之后设置步骤将有助于确保测试总是从相同的状态开始运行.

测试相关操作

要测试所有可能的用例,单元测试是更好的选择.

集成测试更侧重于模块之间的连接,因此是测试 快乐的场景 通常是这样的,因为它将涵盖模块之间的重要连接.

可理解的测试和断言

一个快速的测试视图应该告诉读者正在测试什么, 环境是如何设置的, 什么是存根?, 执行测试的时间, 它所断言的是什么. 断言应该是简单的,并使用帮助程序进行更好的比较和记录.

简单的测试设置

将测试恢复到初始状态应该尽可能简单和容易理解.

避免测试第三方代码

虽然第三方服务可能在测试中使用,但没有必要对它们进行测试. 如果你不信任它们,你可能就不应该使用它们.

将生产代码与测试代码分开

生产代码应该是干净和直接的. 混合测试代码和生产代码 会导致两个不可连接的域耦合在一起吗.

相关的日志

如果没有良好的日志记录,失败的测试就没有多大价值.

当测试通过时,不需要额外的日志记录. 但当它们失败时,广泛的日志记录是至关重要的.

日志记录应该包含所有数据库查询, API请求和响应, 以及对所断言的内容进行全面比较. 这可以极大地方便调试.

好的测试看起来干净易懂

遵循本文指导原则的简单测试可能如下所示:

Const co = require('co'); 
Const 测试 = require('blue-tape'); 
Const 工厂 = require('工厂');
const superTest = require('../跑龙套/ super_测试 ');
const 测试Environment = require('../跑龙套/ 测试_environment_preparer ');  

Const path = '/v1/admin/recipes'; 

测试(`API GET ${path}`.Wrap (函数* (t) { 
	收益率测试Environment.准备();
	Const recipe1 = yield 工厂.创建(“食谱”); 
	Const recipe2 = yield 工厂.创建(“食谱”); 

	const serverResponse = yield superTest.(路径); 

	t.deepEqual (serverResponse.Body, [recipe1, recipe2]); 
}));

上面的代码正在测试一个API (GET / v1 /管理/食谱),它期望它返回一个保存的食谱数组作为响应.

您可以看到,尽管这个测试可能很简单,但它依赖于很多实用程序. 这对于任何好的集成测试套件来说都是常见的.

辅助组件使编写易于理解的集成测试变得容易.

让我们回顾一下集成测试需要哪些组件.

辅助组件

一个全面的测试套件有几个基本的成分, 包括:流量控制, 测试框架, 数据库处理程序, 以及连接后端api的方法.

流控制

JavaScript测试中最大的挑战之一是异步流.

回调函数可以 造成严重破坏 在代码和承诺是不够的. 这就是流帮助程序变得有用的地方.

在等待的时候 异步/等待 为了得到完全支持,可以使用具有类似行为的库. 目标是编写具有异步流可能性的可读、富有表现力和健壮的代码.

Co 使代码能够以一种很好的方式编写,同时保持它不阻塞. 这是通过定义co生成器函数然后生成结果来完成的.

另一个解决方案是使用 蓝知更鸟. 蓝知更鸟是一个承诺库,它有非常有用的特性,比如处理数组, 错误, time, 等.

Co和蓝知更鸟协程的行为类似于ES7中的异步/等待(在继续之前等待解析), 唯一的区别是它总是返回一个承诺, 哪个对处理错误有用.

测试框架

选择测试框架取决于个人偏好. 我的偏好是一个易于使用的框架, 没有副作用, 哪些输出是易于阅读的和管道化的.

JavaScript中有各种各样的测试框架. 在我们的例子中,我们使用 磁带. 磁带, 在我看来, 不仅满足这些要求, 而且比其他测试框架(如Mocha或Jasmin)更干净、更简单.

磁带是基于 测试任何协议(TAP).

TAP对大多数编程语言都有变体.

磁带将测试作为输入,运行测试,然后将结果作为TAP输出. 然后,TAP结果可以通过管道传输到测试报告器,或者以原始格式输出到控制台. 磁带从命令行运行.

磁带有一些很好的特性, 比如在运行整个测试套件之前定义一个要加载的模块, 提供一个小而简单的断言库, 并定义在测试中应该调用的断言的数量. 使用模块预加载可以简化测试环境的准备,并删除任何不必要的代码.

工厂库

工厂库允许您用一种更灵活的方式替换静态fixture文件,从而为测试生成数据. 这样的库允许您定义模型并为这些模型创建实体,而无需编写混乱, 复杂的代码.

JavaScript已经 工厂_girl 为此-图书馆的灵感来自 有相似名字的宝石它最初是为Ruby on Rails开发的.

Const 工厂 = require('工厂-girl').工厂; 
const User = require('../模型/用户”); 

工厂.define('user', user, {username: 'Bob', number_of_recipes: 50}); 

Const user = 工厂.构建(“用户”);

首先,必须在工厂_girl中定义一个新模型.

它是用名称指定的, 项目中的一个模型, 以及从中生成新实例的对象.

另外, 而不是定义从中生成新实例的对象, 可以提供一个返回对象或承诺的函数.

当创建一个模型的新实例时,我们可以:

  • 覆盖新生成实例中的任何值
  • 将附加值传递给构建函数选项

让我们看一个例子.

Const 工厂 = require('工厂-girl').工厂; 
const User = require('../模型/用户”); 

工厂.define('user', User, (buildOptions) => {
	返回{
		名称:“迈克”,
		姓:“道”,
		电子邮件:buildOptions.电子邮件|| mike@gmail.com”
	}
}); 

Const user1 = 工厂.构建(“用户”);
/ /{“名称”:“迈克”,“姓”:“道”,“电子邮件”:“mike@gmail.com”}

Const user2 = 工厂.build('user', {name: 'John'}, {email: 'john@gmail . '.com "});
/ /{“名称”:“约翰”,“姓”:“道”,“电子邮件”:“john@gmail.com”}

连接api

启动一个成熟的HTTP服务器并发出一个实际的HTTP请求, 仅仅在几秒钟后就将其拆除—特别是在进行多个测试时—是完全没有效率的,并且可能导致集成测试花费的时间比必要的要长得多.

SuperTest 是一个调用api的JavaScript库,而不需要创建一个新的活动服务器. 它基于SuperAgent,这是一个用于创建TCP请求的库. 有了这个库,就不需要创建新的TCP连接. api几乎是立即调用的.

支持promise的SuperTest就是这样 super测试-as-promised. 当这样的请求返回一个承诺, 它允许您避免多个嵌套的回调函数, 这样处理流就容易多了.

Const express = require('express') 
Const request = require('super测试-as-promise '); 

Const app = express(); 
请求(应用).get(" /食谱”).then(res => 断言(....)); 

超测服是为快车做的.Js框架,但稍加修改,也可以与其他框架一起使用.

其他实用程序

在某些情况下, 我们需要在代码中模拟一些依赖项, 使用间谍测试函数周围的逻辑, 或者在某些地方使用存根. 这就是这些实用程序包派上用场的地方.

SinonJS 一个伟大的库支持间谍、存根和模拟测试吗. 它还支持其他有用的测试特性, 比如弯曲时间, 测试沙箱, 扩展断言, 还有假的服务器和请求.

在某些情况下,需要模拟代码中的某些依赖项. 对我们想要模拟的服务的引用由系统的其他部分使用.

为了解决这个问题,我们可以使用 依赖注入 或者,如果这不是一个选项,我们可以使用嘲弄服务,如嘲弄.

嘲笑 帮助模拟具有外部依赖的代码. 要正确使用它,应该在加载测试或代码之前调用mock.

Const mock = require('mock '); 
嘲笑.使({ 
warnOnReplace:假的, 
warnOnUnregistered:假 
}); 

const mockingStripe = require('lib/services/internal/stripe'); 嘲笑.registerMock (lib /服务/内部/条纹,mockingStripe);

有了这个新的引用(在本例中), mockingStripe),在稍后的测试中更容易模拟服务.

const stubStripeTransfer = 西农.存根(mockingStripe transferAmount);
stubStripeTransfer.返回(承诺.解决(null));

在Sinon库的帮助下,它很容易被嘲笑. 这里唯一的问题是这个存根会传播到其他测试. 要对其进行沙盒处理,可以使用西农沙盒. 有了它,以后的测试可以将系统恢复到初始状态.

Const 沙盒 = require('西农').沙盒.create ();
const stubStripeTransfer = 沙盒.西农.存根(mockingStripe transferAmount);
stubStripeTransfer.返回(承诺.解决(null));

//在测试之后,或者在开始新测试时更好

沙盒.恢复();

需要其他组件来实现以下功能:

  • 清空数据库(可以通过一个层次结构预构建查询完成)
  • 将其设置为工作状态(sequelize-fixtures)
  • 模拟对第三方服务的TCP请求()
  • 使用更丰富的断言()
  • 保存来自第三方的回应(简单的方法)

没有这么简单的测试

抽象和可扩展性是构建有效集成测试套件的关键要素. 所有将焦点从测试的核心(准备数据)移开的东西, 操作(Action)和断言(断言ion)应该分组并抽象为实用函数.

虽然这条路没有对错之分, 因为一切都取决于项目及其需求, 一些关键的品质对于任何好的集成测试套件来说都是通用的.

下面的代码展示了如何测试一个API,该API创建了一个食谱,并作为副作用发送了一封电子邮件.

它存根外部电子邮件提供商,以便您可以测试如果电子邮件已经发送,而没有实际发送一个. 测试还验证API是否使用适当的状态码进行响应.

Const co = require('co'); 
Const 工厂 = require('工厂');
const superTest = require('../跑龙套/ super_测试 ');  
const basicEnv = require('../跑龙套/ basic_测试_enivornment '); 

Const path = '/v1/admin/recipes'; 

basicEnv.测试(' API POST ${path} '.Wrap (函数* (t, 断言, 沙盒) { 
	Const chef = yield 工厂.创建(“厨师”); 
	常量body = {
		chef_id:厨师.id,
		recipe_name:“蛋糕”,
		配料:“胡萝卜”、“巧克力”、“饼干”
	}; 
	
	Const stub = 沙盒.存根(嘲弄.emailProvider, sendNewEmail).returnsPromise(空);
	const serverResponse = yield superTest.(道路、身体); 
	
	断言.间谍(存根).(1);
	断言.statusCode (serverResponse, 201);
}));

上面的测试是可重复的,因为它每次都从一个干净的环境开始.

它有一个简单的设置过程,其中与设置相关的所有内容都合并在 basicEnv.测试 函数.

它只测试一个动作——一个API. 它通过简单的断言语句清楚地说明了测试的期望. 此外,测试不会通过存根/模拟来涉及第三方代码.

开始编写集成测试

在将新代码推向生产环境时, 开发人员(以及所有其他项目参与者)希望确保新功能可以正常工作,旧功能不会损坏.

如果没有测试,这是很难实现的, 如果做得不好可能会导致挫败感, 项目的疲劳, 最终导致项目失败.

集成测试,结合单元测试,是第一道防线.

只使用两者中的一个是不够的,并且会为未发现的错误留下大量空间. 始终使用这两种方法将使新的提交更加健壮, 并在所有项目参与者中传递信心并激发信任.

聘请Toptal这方面的专家.
现在雇佣
卢卡Blažecki

卢卡Blažecki

验证专家 在工程

克罗地亚的萨格勒布

2015年9月12日成为会员

作者简介

Luka (MCS)是一个专注于可扩展后端解决方案的团队领导者. 他精通节点.js、SQL和NoSQL数据库以及AWS.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

以前在

infobip

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.