Skip to main content

单元测试

¥Unit testing

🤓

Strapi 博客上有一个关于如何实现 使用 Jest 和 Supertest 进行 API 测试如何将单元测试添加到你的 Strapi 插件 的教程。

¥The Strapi blog has a tutorial on how to implement API testing with Jest and Supertest and how to add unit tests to your Strapi plugin.

在本指南中,我们将了解如何使用测试框架为 Strapi 应用运行基本单元测试。

¥In this guide we will see how you can run basic unit tests for a Strapi application using a testing framework.

在此示例中,我们将使用 Jest 测试框架(重点关注简单性)和 超测 超级代理驱动库,以使用流畅的 API 测试 Node.js HTTP 服务器。

¥In this example we will use Jest Testing Framework with a focus on simplicity and Supertest Super-agent driven library for testing node.js HTTP servers using a fluent API.

提醒

请注意,如果你在 Windows 上使用 SQLite 数据库,由于 Windows 锁定 SQLite 文件的方式,本指南将不起作用。

¥Please note that this guide will not work if you are on Windows using the SQLite database due to how windows locks the SQLite file.

安装测试工具

¥Install test tools

Jest 包含一组用于创建和设计测试用例的指南或规则 - 旨在帮助测试人员更有效地进行测试的实践和工具的组合。

¥Jest contains a set of guidelines or rules used for creating and designing test cases - a combination of practices and tools that are designed to help testers test more efficiently.

Supertest 允许你测试所有 api 路由,因为它们是 http.Server 的实例。

¥Supertest allows you to test all the api routes as they were instances of http.Server.

sqlite3 用于创建在测试之间创建和删除的磁盘数据库。

¥sqlite3 is used to create an on-disk database that is created and deleted between tests.

yarn add --dev jest supertest sqlite3

完成后将其添加到 package.json 文件中

¥Once this is done add this to package.json file

test 命令添加到 scripts 部分

¥add test command to scripts section

  "scripts": {
"develop": "strapi develop",
"start": "strapi start",
"build": "strapi build",
"strapi": "strapi",
"test": "jest --forceExit --detectOpenHandles"
},

并在文件底部添加这些行

¥and add those lines at the bottom of file

  "jest": {
"testPathIgnorePatterns": [
"/node_modules/",
".tmp",
".cache"
],
"testEnvironment": "node"
}

这些将通知 Jest 不要在不应查找的文件夹内查找测试。

¥Those will inform Jest not to look for test inside the folder where it shouldn't.

设置测试环境

¥Set up a testing environment

测试框架必须有一个干净的空环境来执行有效的测试,并且不干扰当前的数据库。

¥Test framework must have a clean empty environment to perform valid test and also not to interfere with current database.

一旦 jest 运行,它就会使用 test environment(将 NODE_ENV 切换到 test),因此我们需要为此目的创建一个特殊的环境设置。为测试环境 ./config/env/test/database.js 创建一个新配置并添加以下值 "filename": ".tmp/test.db" - 原因是我们希望有一个单独的 sqlite 数据库用于测试,因此我们的测试不会触及真实数据。该文件将是临时的,每次测试完成时,我们将删除每次在干净的数据库上运行测试时的该文件。整个文件将如下所示:

¥Once jest is running it uses the test environment (switching NODE_ENV to test) so we need to create a special environment setting for this purpose. Create a new config for test env ./config/env/test/database.js and add the following value "filename": ".tmp/test.db" - the reason of that is that we want to have a separate sqlite database for tests, so our test will not touch real data. This file will be temporary, each time test is finished, we will remove that file that every time tests are run on the clean database. The whole file will look like this:

path: ./config/env/test/database.js

module.exports = ({ env }) => ({
connection: {
client: 'sqlite',
connection: {
filename: env('DATABASE_FILENAME', '.tmp/test.db'),
},
useNullAsDefault: true,
debug: false
},
});

创建 Strapi 实例

¥Create a Strapi instance

为了测试任何内容,我们需要有一个在测试环境中运行的 Strapi 实例,基本上我们希望将 Strapi 应用的实例作为对象获取,类似于为 进程管理 创建实例。

¥In order to test anything we need to have a strapi instance that runs in the testing environment, basically we want to get instance of strapi app as object, similar like creating an instance for process manager.

这些任务需要添加一些文件 - 让我们创建一个文件夹 tests,所有测试都将放在其中,并位于文件夹 helpers 旁边,其中主 Strapi 助手将位于文件 Strapi.js 中。

¥These tasks require adding some files - let's create a folder tests where all the tests will be put and inside it, next to folder helpers where main Strapi helper will be in file strapi.js.

path: ./tests/helpers/strapi.js


const Strapi = require("@strapi/strapi");




const fs = require("fs");



let instance;

async function setupStrapi() {
if (!instance) {
await Strapi().load();
instance = strapi;

await instance.server.mount();
}
return instance;
}

async function cleanupStrapi() {
const dbSettings = strapi.config.get("database.connection");

//close server to release the db-file
await strapi.server.httpServer.close();

// close the connection to the database before deletion
await strapi.db.connection.destroy();

//delete test database after all tests have completed
if (dbSettings && dbSettings.connection && dbSettings.connection.filename) {
const tmpDbFile = dbSettings.connection.filename;
if (fs.existsSync(tmpDbFile)) {
fs.unlinkSync(tmpDbFile);
}
}
}

module.exports = { setupStrapi, cleanupStrapi };

测试 Strapi 实例

¥Test a Strapi instance

我们需要一个用于测试的主入口文件,该文件也将测试我们的帮助文件。

¥We need a main entry file for our tests, one that will also test our helper file.

path: ./tests/app.test.js


const fs = require('fs');


const { setupStrapi, cleanupStrapi } = require("./helpers/strapi");

beforeAll(async () => {
await setupStrapi();
});

afterAll(async () => {
await cleanupStrapi();
});

it("strapi is defined", () => {
expect(strapi).toBeDefined();
});

实际上,这就是我们编写单元测试所需的全部内容。只需运行 yarn test 并查看第一次测试的结果

¥Actually this is all we need for writing unit tests. Just run yarn test and see a result of your first test

yarn run v1.13.0
$ jest
PASS tests/app.test.js
✓ strapi is defined (2 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.187 s
Ran all test suites.
✨ Done in 5.73s.
💡 提示

如果你收到 Jest 超时错误,请在 app.test.js 文件中的 beforeAll 方法之前添加以下行:jest.setTimeout(15000) 并根据需要调整毫秒值。

¥If you receive a timeout error for Jest, please add the following line right before the beforeAll method in the app.test.js file: jest.setTimeout(15000) and adjust the milliseconds value as you need.

测试基本端点控制器

¥Test a basic endpoint controller

💡 提示

在示例中,我们将使用 controllers 部分中的示例 Hello world /hello 端点。

¥In the example we'll use and example Hello world /hello endpoint from controllers section.

有人可能会说 API 测试不是单元测试,而是有限的集成测试,无论术语如何,让我们继续测试第一个端点。

¥Some might say that API tests are not unit but limited integration tests, regardless of nomenclature, let's continue with testing first endpoint.

我们将测试我们的端点是否正常工作并且路由 /hello 是否返回 Hello World

¥We'll test if our endpoint works properly and route /hello does return Hello World

让我们创建一个单独的测试文件,其中 supertest 将用于检查端点是否按预期工作。

¥Let's create a separate test file where supertest will be used to check if endpoint works as expected.

path: ./tests/hello/index.js



const request = require('supertest');



it("should return hello world", async () => {
await request(strapi.server.httpServer)
.get("/api/hello")
.expect(200) // Expect response http code 200
.then((data) => {
expect(data.text).toBe("Hello World!"); // expect the response text
});
});

然后将此代码包含到该文件底部的 ./tests/app.test.js

¥Then include this code to ./tests/app.test.js at the bottom of that file

require('./hello');

并运行 yarn test 应该返回

¥and run yarn test which should return

➜  my-project yarn test
yarn run v1.13.0
$ jest --detectOpenHandles
PASS tests/app.test.js (5.742 s)
✓ strapi is defined (4 ms)
✓ should return hello world (208 ms)

[2020-05-22T14:37:38.018Z] debug GET /hello (58 ms) 200
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 6.635 s, estimated 7 s
Ran all test suites.
✨ Done in 9.09s.
💡 提示

如果你收到错误 Jest has detected the following 1 open handles potentially keeping Jest from exiting,请检查 jest 版本,因为 26.6.3 可以正常工作。

¥If you receive an error Jest has detected the following 1 open handles potentially keeping Jest from exiting check jest version as 26.6.3 works without an issue.

测试 auth 端点控制器

¥Test an auth endpoint controller

在这种情况下,我们将通过两个测试来测试身份验证登录端点

¥In this scenario we'll test authentication login endpoint with two tests

  1. 测试 /auth/local 应该登录用户并返回 jwt 令牌

    ¥Test /auth/local that should login user and return jwt token

  2. 测试 /users/me 应根据 Authorization 标头返回用户数据

    ¥Test /users/me that should return users data based on Authorization header

path: ./tests/user/index.js


const request = require('supertest');



// user mock data


const mockUserData = {


username: "tester",
email: "tester@strapi.com",
provider: "local",
password: "1234abc",
confirmed: true,
blocked: null,
};

it("should login user and return jwt token", async () => {
/** Creates a new user and save it to the database */
await strapi.plugins["users-permissions"].services.user.add({
...mockUserData,
});

await request(strapi.server.httpServer) // app server is an instance of Class: http.Server
.post("/api/auth/local")
.set("accept", "application/json")
.set("Content-Type", "application/json")
.send({
identifier: mockUserData.email,
password: mockUserData.password,
})
.expect("Content-Type", /json/)
.expect(200)
.then((data) => {
expect(data.body.jwt).toBeDefined();
});
});

it('should return users data for authenticated user', async () => {
/** Gets the default user role */
const defaultRole = await strapi.query('plugin::users-permissions.role').findOne({}, []);

const role = defaultRole ? defaultRole.id : null;

/** Creates a new user an push to database */
const user = await strapi.plugins['users-permissions'].services.user.add({
...mockUserData,
username: 'tester2',
email: 'tester2@strapi.com',
role,
});

const jwt = strapi.plugins['users-permissions'].services.jwt.issue({
id: user.id,
});

await request(strapi.server.httpServer) // app server is an instance of Class: http.Server
.get('/api/users/me')
.set('accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Authorization', 'Bearer ' + jwt)
.expect('Content-Type', /json/)
.expect(200)
.then(data => {
expect(data.body).toBeDefined();
expect(data.body.id).toBe(user.id);
expect(data.body.username).toBe(user.username);
expect(data.body.email).toBe(user.email);
});
});

然后将此代码包含到该文件底部的 ./tests/app.test.js

¥Then include this code to ./tests/app.test.js at the bottom of that file

require('./user');

上面的所有测试都应该返回类似的控制台输出

¥All the tests above should return an console output like

➜  my-project git:(master) yarn test

yarn run v1.13.0
$ jest --forceExit --detectOpenHandles
[2020-05-27T08:30:30.811Z] debug GET /hello (10 ms) 200
[2020-05-27T08:30:31.864Z] debug POST /auth/local (891 ms) 200
PASS tests/app.test.js (6.811 s)
✓ strapi is defined (3 ms)
✓ should return hello world (54 ms)
✓ should login user and return jwt token (1049 ms)
✓ should return users data for authenticated user (163 ms)

Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 6.874 s, estimated 9 s
Ran all test suites.
✨ Done in 8.40s.