单元测试 Testing

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

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

  • 请注意,如果您在 Windows 上使用 SQLite 数据库,则本指南将不起作用,因为 Windows 锁定 SQLite 文件的方式。
  • 此页面正在进行中,可能已过时。一旦 Strapi v5 推出,它将被大量重新制作。

安装测试工具

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

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

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

bash
yarn
bash
yarn add --dev jest supertest sqlite3

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

test 命令添加到 scripts 部分

package.json
json
{
  "scripts": {
    "develop": "strapi evolve",
    "start": "strapi start",
    "build": "strapi build",
    "strapi": "strapi",
    "test": "jest --forceExit --detectOpenHandles"
  }
}

并在文件底部添加这些行

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

这些将通知Jest不要在它不应该的文件夹中寻找测试。

设置测试环境

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

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

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

创建 Strapi 实例

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

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

path: ./tests/helpers/strapi.js
js
const fs = require('node:fs')
const Strapi = require('@strapi/strapi')

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')

  // 关闭服务器以释放数据库文件
  await strapi.server.httpServer.close()

  // 删除前关闭与数据库的连接
  await strapi.db.connection.destroy()

  // 所有测试完成后删除测试数据库
  if (dbSettings && dbSettings.connection && dbSettings.connection.filename) {
    const tmpDbFile = dbSettings.connection.filename
    if (fs.existsSync(tmpDbFile)) {
      fs.unlinkSync(tmpDbFile)
    }
  }
}

module.exports = { setupStrapi, cleanupStrapi }

测试 Strapi 实例

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

path: ./tests/app.test.js
js
const fs = require('node:fs')
const { setupStrapi, cleanupStrapi } = require('./helpers/strapi')

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

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

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

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

bash
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),并根据需要调整毫秒值。

测试基本端点控制器

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

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

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

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

path: ./tests/hello/index.js
js
const request = require('supertest')

it('should return hello world', async () => {
  await request(strapi.server.httpServer)
    .get('/api/hello')
    .expect(200) // 期望响应 http 代码 200
    .then((data) => {
      expect(data.text).toBe('Hello World!') // 期望响应文本
    })
})

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

js
require('./hello')

然后运行yarn test,它应该返回

bash
  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 检测到以下 1 个打开的句柄可能阻止 Jest 退出”,请检查“jest”版本,因为“26.6.3”可以正常工作。

测试 auth 端点控制器

在此场景中,我们将使用两个测试来测试身份验证登录端点

  1. 测试 /auth/local,它应登录用户并返回 jwt 令牌
  2. 测试 /users/me,它应根据 Authorization 标头返回用户数据
path: ./tests/user/index.js
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 () => {
  /** 创建新用户并将其保存到数据库 */
  await strapi.plugins['users-permissions'].services.user.add({
    ...mockUserData,
  })

  await request(strapi.server.httpServer) // 应用服务器是类的一个实例: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 () => {
  /** 获取默认用户角色 */
  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) // 应用服务器是类的一个实例: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

js
require('./user')

上述所有测试都应返回类似这样的控制台输出

bash
  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.