Jest是一个基于JavaScript的测试框架,可以让你同时测试前端和后端应用程序。 Jest很适合验证,因为它捆绑了一些工具,使编写测试更容易管理。虽然Jest最常被用于简单的API测试场景和断言,但它也可用于测试复杂的数据结构。在本教程中,我们将了解如何使用Jest来测试嵌套的API响应,并希望在这个过程中获得乐趣。
前提条件
要完成本教程,你将需要。
- 在您的系统上安装Node.js
- 一个CircleCI账户
- 一个GitHub账户
- Postman或任何其他HTTP客户端来测试API
- 对JavaScript的基本了解
- 对API和单元测试的基本了解
克隆存储库
本教程将带领你完成为一个假的空间API项目编写Jest测试的过程。要访问整个项目,请通过运行此命令克隆仓库。
git clone --single-branch --branch base-project https://github.com/CIRCLECI-GWP/space-api
cd space-api
创建我们的空间API
为了开始编写我们的Jest测试,我们首先需要运行我们的API。这就是我们要写的测试对象。在本教程中,我们将使用一个简单的API,它将提供我们可以用来编写测试的响应。我选择了一个基于太空旅行的API,所以准备好未来的太空探险家吧
本教程的API已经被开发出来了。用这个命令设置它,安装应用程序的依赖性。
npm install
启动应用程序(API服务器)。
npm start
运行这些命令后,我们可以开始编写测试。
该API返回太空旅行所需的最关键项目的响应。它有这些端点。
- 可用的空间目的地
- 可用的航班和相关信息
- 飞行座位及其配置的选择
注意: *本教程的重点是用Jest进行测试,而不是开发一个空间API库,所以没有这方面的步骤。如果你对所有可用的端点感到好奇,或者对应用程序的架构感到好奇,请到这里查看应用程序*的路由文件。
假设你已经在你的机器上克隆并设置了应用程序,所有的端点都已经存在。用API客户端(如Postman)向http://localhost:3000/space/flights,http://localhost:3000/space/destinations 和http://localhost:3000/space/flights/seats 发出一个GET 请求。
这些细节已经被硬编码到API端点上。



现在你已经体验了我们的API是如何工作和模拟响应的,我们可以继续进行测试了。
用Jest编写API测试
在上一节中,我们设置了一个API来接收来自不同API路由的响应。在这一节中,我们将深入测试我们的API响应,了解Jest如何用于测试嵌套的JavaScript对象和数组,并了解这将如何提高你作为一个开发人员或测试工程师的测试技能。我们将使用JEST来测试我们的API响应的不同组合,这些组合从简单的数组到包含对象的数组的复杂响应,甚至包含数组的对象与更多的子数组和对象。
安装Jest
为了开始使用Jest进行测试,我们需要将其添加到我们目前已有的空间API中。要把Jest作为一个开发依赖项安装,请运行这个命令。
npm install --save-dev jest
注意: 开发依赖是指仅为开发目的而需要的依赖。这些依赖不应该被安装在生产环境中。
成功运行安装后,我们需要对我们的package.json 文件做一些修改,以确保我们的空间测试以适当的配置运行。我们将添加两个命令,一个命令用于测试我们的API端点,另一个命令用于请求Jest观察我们文件的变化并重新运行测试。在package.json文件中添加Jest命令。
"scripts": {
"start": "node ./bin/www",
"test": "jest"
},
你可以添加一个Jest配置文件来代替,如果这对你的项目更有效的话。不过在本教程中,我们将使用Jest的默认配置。
在Jest中设置API测试
对于我们的第一个测试,在根目录下创建一个tests 文件夹。在这个文件夹中,创建一个测试文件,即main routes,并在其中添加.spec.js 扩展。在我们的例子中,这将是space.spec.js 。添加.spec.js 扩展名告诉Jest,该文件是一个测试文件。
像航天飞机发射一样,我们用静态火开始我们的测试过程,以检查我们的测试配置中的一切都在正常工作。
在tests/space.spec.js 文件中,用这段代码实现这个测试。
describe('Space test suite', () => {
it('My Space Test', () => {
expect(true).toEqual(true);
});
});
现在,当你在终端上运行npm test ,你将得到一个成功执行的测试。当测试运行并通过时,终端上的测试结果应该是绿色的。你可以进一步将断言从true 改为false ,终端上的执行结果将是红色,表示失败。
在我们开始火箭发射序列之前,我们应该在我们的package.json ,添加Jest的watch 命令。添加这个命令意味着当我们的应用程序代码改变时,测试会被重新运行。当你的应用程序在一个终端运行而你的测试在另一个终端时,这是有可能的。在package.json 文件中添加这个命令。
"scripts": {
"start": "node ./bin/www",
"test": "jest",
"test:watch": "npm run test -- --watch"
},
当你在运行watch命令时执行你的Node.js API,当你改变任何代码时,你可以实时观察我们测试的重新运行。这可以在检测你的API因改变而中断时派上用场。在一个终端窗口中运行应用程序,与测试并排,显示观察选项(在右边)。

在CircleCI上设置项目
我们的下一步是设置一个CircleCI管道,以便在我们运行测试时使用。该管道可以帮助你验证一切都在按预期工作。要设置CircleCI,你首先需要通过运行命令在你的项目中初始化一个GitHub仓库。
git init
接下来,在根目录下创建一个.gitignore 文件。在该文件内添加node_modules 。在.gitignore ,可以防止npm生成的模块被添加到你的远程仓库。现在,添加一个提交,然后把你的项目推送到GitHub。
登录CircleCI,点击侧面菜单上的Projects,进入Projects仪表板。那里会有一个与你的GitHub用户名或你的组织相关的所有GitHub仓库的列表。

点击设置项目的教程项目(它与GitHub仓库的名称相同)。点击Let's Go。

点击跳过此步骤。

点击Use Existing Config。

在提示中,点击开始构建。我们预计我们的管道会失败,因为我们仍然需要在GitHub上添加我们定制的config.yml 配置文件,这样项目才能正常构建。

编写CI流水线
一旦我们建立了管道,现在是时候把CircleCI添加到我们的本地项目中了。首先,在根目录下创建一个.circleci 文件夹。在该文件夹中,创建一个config.yml 文件。现在,通过配置,将细节添加到config.yml 文件中,如该代码块所示。
version: 2.1
jobs:
build:
working_directory: ~/repo
docker:
- image: cimg/node:10.16.3
steps:
- checkout
- run:
name: update npm
command: "npm install -g npm@5"
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: install dependencies
command: npm install
- save_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
paths:
- ./node_modules
- run:
name: run space test
command: npm test
- store_artifacts:
path: ~/repo/space
在这个配置中,CircleCI使用一个node Docker镜像,从环境中拉出,然后更新npm包管理器。下一个阶段是恢复缓存,如果它存在的话。只有当用save-cache 检测到变化时,才会更新应用程序的依赖关系。最后,我们将运行我们的空间测试,并将缓存的项目存储在工件的space 目录中。
写完这个配置后,将你的修改提交到git,并将你的修改推送到GitHub。CircleCI应该自动开始构建过程,并且应该通过,只运行我们的不那么有意义的测试。
测试我们的空间API
为了在我们的Jest测试中进行API请求,我们需要一个模块来查询我们的端点并返回响应给我们的测试。这个模块就是SuperTest,你可以用这个命令安装它。
npm install --save-dev supertest
Jest和SuperTest已经设置好了,并且在Jest测试框架中编写了一个基本测试。在本节中,我们将重点测试由我们的端点提供的不同API响应对象。从端点提供的响应开始http://localhost:3000/space/destinations 。它是一个简单的对象数组,代表太空飞行将前往的目的地。
[ "Mars", "Moon", "Earth", "Mercury", "Venus", "Jupiter"]
数组是你能从API端点收到的最基本的响应之一,然而Jest为我们提供了无数的方法来断言收到的响应符合我们的期望。
我们的第一个实际测试将集中在这个对象的数组上。用这个代码片断替换space.spec.js 文件的内容。
const request = require('supertest');
const app = require("../app");
describe('Space test suite', () => {
it('tests /destinations endpoints', async() => {
const response = await request(app).get("/space/destinations");
expect(response.body).toEqual(["Mars", "Moon", "Earth", "Mercury", "Venus", "Jupiter"]);
expect(response.body).toHaveLength(6);
expect(response.statusCode).toBe(200);
// Testing a single element in the array
expect(response.body).toEqual(expect.arrayContaining(['Earth']));
});
// Insert other tests below this line
// Insert other tests above this line
});
在这个测试中,我们可以使用SuperTest与API进行交互。该测试首先从/space/destinations 端点获取响应,然后使用该响应对几个断言进行测试。最值得注意的断言是最后一个,它使用Jest数组arrayContaining() 方法来验证是否能在数组中找到一个单项。在这个断言中,Jest通过数组映射,知道哪些数组元素存在于数组中,哪些不存在。
虽然上面的数组只包含五个项目,但你很可能会遇到更复杂的情况,在这种情况下,如果不把它拆开,你就无法完全测试整个对象。
在下一个测试中,我们将测试/space/flights/seats 端点。这个端点提供了一个比上面的数组更复杂的响应,我们将再次使用Jest来测试它。
考虑到我们的响应的性质,我们将使用从端点收到的响应的一部分,这将给我们一个测试模板,我们期望为响应中的后续对象编写测试。
{ "starship": [
{
"firstClass": {
"heatedSeats": true,
"chairOptions": [
"leather",
"wollen"
],
"vaultAccess": true,
"drinksServed": [
"tea",
"coffee",
"space-special",
"wine"
],
"windowAccess": true,
"privateCabin": "2",
"VRAccess": "unlimited",
"cost": "$20000",
"seatHover": {
"cryoMode": [
"extreme",
"ludacris",
"plaid"
],
"staticMode": [
"ludacris",
"plaid"
]
}
},
"businessClass": { ... }
}
],
"blueOrigin": [
{
"firstClass": { ... },
"businessClass": { ... }
}
]
}
注意: 你可以通过使用Postman等工具调用端点来查看完整的响应。
这个测试的重点是为对象写一个详尽的测试,同时确保测试的可读性和简单。考虑到响应的复杂性质,我们将在保持测试的可读性的同时,尽可能多的覆盖。
...
it('tests /space/flights/seats endpoint - starship', async () => {
const response = await request(app).get("/space/flights/seats");
expect(response.body.starship).toEqual(expect.arrayContaining([expect.any(Object)]));
// Checking that the starship Object contains firstClass Object which then contains a set of objects
expect(response.body.starship).toEqual(expect.arrayContaining(
[expect.objectContaining({ firstClass: expect.any(Object) })]));
expect(response.body.starship).toEqual(expect.arrayContaining([expect.objectContaining(
{ businessClass: expect.any(Object) })]));
// Checking that under the bussinessClass Object we have the array drinks served
expect(response.body.starship)
.toEqual(expect.arrayContaining([expect.objectContaining({
businessClass: expect.objectContaining({ drinksServed: expect.any(Array) })
})]));
// Checking that under the firstClass: Object we have the option ludacris in the seatHover Object
expect(response.body.starship)
.toEqual(expect.arrayContaining([expect.objectContaining({
firstClass: expect.objectContaining({ seatHover: expect.objectContaining({
cryoMode : expect.arrayContaining(['ludacris'])}) })
})]));
// Checking that under the firstClass: Object we have the option plaid in the seatHover Object
expect(response.body.starship)
.toEqual(expect.arrayContaining([expect.objectContaining({
firstClass: expect.objectContaining({ seatHover: expect.objectContaining({
staticMode : expect.arrayContaining(['plaid'])}) })
})]));
});
...
这个测试使用Jest匹配器遍历了上面提供的对象的不同层次。它首先测试顶层对象,然后缩小到对象和已经深入嵌套到响应中的数组。这个测试一开始可能看起来很吓人,但它表明我们可以专门测试一个API的响应区域,而不用断言整个响应对象。
在下一节中,我们将学习如何在发射到太空之前在Jest中编写自定义的错误信息!
使用Jest自定义匹配器来发送错误信息
Jest匹配器让你以不同方式测试数据。例如,你可以明确地测试一个特定的条件,甚至在现有的匹配器没有涵盖该条件的情况下扩展你自己的自定义匹配器。Jest提供了一种声明自定义匹配器的方法,在运行测试时有自己的自定义错误。一旦我们进入太空,通过覆盖命令中心给出的命令,拥有一定程度的控制权将是非常好的。我们不希望撞上陨石或空间碎片,如果有什么我们可以做的事情来防止它。要做到这一点,我们将使用我们上面创建的/space/flights API端点的响应。
Jest提供了expect.extend(matchers) 方法,所以你可以添加自定义期望。在本教程中,我们要验证我们只能预订那些active 的航班。在我们创建的航班端点中,该对象返回不同的航班,其活动状态要么设置为true ,要么设置为false 。相反,我们可以尝试为此编写一个匹配器。
要创建一个使用自定义匹配器的测试,我们需要声明匹配器并定义哪些参数会导致通过或失败。这个匹配器可以在Jestdescribe 块的任何地方声明。为了便于阅读,请将其放在测试文件的顶部,紧跟在描述块之后。在这个项目中,我们想检查返回的航班是否有活动状态。
...
expect.extend({
toBeActive(received) {
const pass = received === true;
if (pass) {
return {
message: () =>
`expected ${received} is an acceptable flight status`,
pass: true,
};
}
}
});
...
自定义匹配器期望收到航班状态的值,并返回一个自定义消息。但有一个小问题。当我们运行测试时,即使测试不符合传递的航班状态值的预期,也会得到错误的消息。换句话说,是一个假的否定。为了解决这个问题,添加一个else 语句,以适应我们的断言会失败的情况下的标准。else 语句将捕获所有运行测试时可能发生的假阴性,并报告它们。像这样更新代码。
...
expect.extend({
toBeActive(received) {
const pass = received === true;
if (pass) {
return {
message: () =>
`expected ${received} to be an acceptable flight status`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be an acceptable flight status of flight - only true is acceptable`,
pass: false,
};
}
},
});
...
当测试出现假阴性时,第一个自定义消息被启动。第二条是在测试失败时,通过从太空飞行的活动状态接收false 值而启动。
把这个代码块添加到我们之前添加的expect.extend 代码块下面。
...
it('tests /space/flights endpoint - positive test', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[0].active).toBeActive();
});
it('tests /space/flights endpoint - false negative', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[0].active).not.toBeActive();
});
it('tests /space/flights endpoint - failing test', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[1].active).toBeActive();
});
...
当你运行这些测试时,有些测试会失败,有这样的输出。
> jest
FAIL tests/space.spec.js
Space test suite
✓ tests /destinations endpoints (25 ms)
✓ tests /space/flights/seats endpoint - starship (4 ms)
✓ tests /space/flights endpoint - positive test (3 ms)
✕ tests /space/flights endpoint - false negative (2 ms)
✕ tests /space/flights endpoint - failing test (3 ms)
...
GET /space/destinations 200 2.851 ms - 51
GET /space/flights/seats 200 0.699 ms - 1073
GET /space/flights 200 0.610 ms - 1152
GET /space/flights 200 0.382 ms - 1152
GET /space/flights 200 0.554 ms - 1152
Test Suites: 1 failed, 1 total
Tests: 2 failed, 3 passed, 5 total
Snapshots: 0 total
Time: 0.512 s, estimated 1 s
Ran all test suites.
注意: 这两个失败的测试是按原样写的,用于演示。要使这些测试在CI上通过,用it.skip() 函数跳过它们,这样它们就不会被默认运行。
...
it('tests /space/flights endpoint - positive test', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[0].active).toBeActive();
});
it.skip('tests /space/flights endpoint - false negative', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[0].active).not.toBeActive();
});
it.skip('tests /space/flights endpoint - failing test', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[1].active).toBeActive();
});
...
然后我们就有了通过的测试。反查GitHub上的tests/space.spec.js 文件的最终状态。

验证流水线的成功
当你的测试正常,并且CI管道如前所述设置好后,将所有文件添加到git,并推送到你的GitHub远程仓库。CircleCI管道应启动并自动运行测试。为了观察流水线的执行情况,进入CircleCI仪表盘,选择教程项目的名称,这与GitHub仓库的名称相似:space-api 。
在CircleCI仪表板上点击构建,查看CircleCI配置文件中定义的每一步的状态。

总结
在本教程中,你已经设置了一个space-api,安装了Jest,并针对API编写了测试。你还学会了如何使用Jest匹配器,甚至编写你自己的自定义匹配器。通过本教程,我们还介绍了如何将CircleCI集成到一个项目中,将测试推送到GitHub,并为CI管道创建不同的构建步骤。最后,我们涵盖了在管道上运行我们的测试,并验证管道的所有步骤都按预期工作。
所有的系统都为我们发射到太空而准备好了。T-minus-zero快乐!