单元和集成测试指南
🌐 Unit and integration testing guide
Page summary:
测试依赖于 Jest 和 Supertest,以及一个内存中的 SQLite 数据库,一个经过修补的 Strapi 测试工具,它还支持 TypeScript 配置文件,以及在设置期间自动注册
/hello路由和认证角色的辅助工具。
本指南提供了一种动手的方法,用于在 Strapi 5 应用中配置 Jest ,为插件代码的单元测试模拟 Strapi 对象,以及使用 Supertest 对 REST 端点进行端到端测试。
该指南旨在重现 strapi-unit-testing-examples CodeSandbox 链接中可用的最小测试套件。
如果你在 Windows 上使用 SQLite 数据库,则本指南将不起作用,因为 Windows 会锁定 SQLite 文件。
🌐 The present guide will not work if you are on Windows using the SQLite database due to how Windows locks the SQLite file.
安装工具
🌐 Install tools
我们将首先安装测试工具,添加运行测试的命令,并配置 Jest。
🌐 We'll first install test tools, add a command to run our tests, and configure Jest.
-
通过在终端中运行以下命令安装 Jest 和 Supertest:
- Yarn
- NPM
yarn add jest supertest --devnpm install jest supertest --save-devJest提供测试运行器和断言工具。Supertest允许你测试所有作为<ExternalLink to="https://nodejs.cn/api/http.html#class-httpserver" text="http.Server"/>实例的api路径。
-
使用以下内容更新你 Strapi 项目的
package.json文件:-
在
scripts部分添加一个test命令,使其看起来如下:"scripts": {
"build": "strapi build",
"console": "strapi console",
"deploy": "strapi deploy",
"dev": "strapi develop",
"develop": "strapi develop",
"seed:example": "node ./scripts/seed.js",
"start": "strapi start",
"strapi": "strapi",
"upgrade": "npx @strapi/upgrade latest",
"upgrade:dry": "npx @strapi/upgrade latest --dry",
"test": "jest --forceExit --detectOpenHandles"
}, -
在文件底部配置 Jest,以忽略 Strapi 构建产物,并映射你从测试中导入的任何根级模块:
"jest": {
"testPathIgnorePatterns": [
"/node_modules/",
".tmp",
".cache"
],
"testEnvironment": "node",
"moduleNameMapper": {
"^/create-service$": "<rootDir>/create-service"
}
}
-
为插件单元测试模拟 Strapi
🌐 Mock Strapi for plugin unit tests
纯单元测试对于 Strapi 插件来说是理想的,因为它们让你在不启动 Strapi 服务器的情况下验证控制器和服务逻辑。使用 Jest 的 mocking 工具来重现 Strapi 对象的部分以及你的代码依赖的任何请求上下文。
控制器示例
🌐 Controller example
创建一个测试文件,例如 ./tests/todo-controller.test.js,该文件使用模拟的 Strapi 对象实例化你的控制器,并验证控制器执行的每一个调用:
🌐 Create a test file such as ./tests/todo-controller.test.js that instantiates your controller with a mocked Strapi object and verifies every call the controller performs:
const todoController = require('./todo-controller');
describe('Todo controller', () => {
let strapi;
beforeEach(() => {
strapi = {
plugin: jest.fn().mockReturnValue({
service: jest.fn().mockReturnValue({
create: jest.fn().mockReturnValue({
data: {
name: 'test',
status: false,
},
}),
complete: jest.fn().mockReturnValue({
data: {
id: 1,
status: true,
},
}),
}),
}),
};
});
it('creates a todo item', async () => {
const ctx = {
request: {
body: {
name: 'test',
},
},
body: null,
};
await todoController({ strapi }).index(ctx);
expect(ctx.body).toBe('created');
expect(strapi.plugin('todo').service('create').create).toHaveBeenCalledTimes(1);
});
it('completes a todo item', async () => {
const ctx = {
request: {
body: {
id: 1,
},
},
body: null,
};
await todoController({ strapi }).complete(ctx);
expect(ctx.body).toBe('todo completed');
expect(strapi.plugin('todo').service('complete').complete).toHaveBeenCalledTimes(1);
});
});
beforeEach 钩子重建了 mock,因此每个测试都从一个干净的 Strapi 实例开始。每个测试会准备控制器所期望的 ctx 请求对象,调用控制器函数,并断言响应以及与 Strapi 服务的交互。
🌐 The beforeEach hook rebuilds the mock so every test starts with a clean Strapi instance. Each test prepares the ctx request object that the controller expects, calls the controller function, and asserts both the response and the interactions with Strapi services.
服务示例
🌐 Service example
服务可以在同一个测试套件中进行测试,也可以在专用文件中通过仅模拟它们调用的 Strapi 查询层进行测试。
🌐 Services can be tested in the same test suite or in a dedicated file by mocking only the Strapi query layer they call into.
const createService = require('./create-service');
describe('Create service', () => {
let strapi;
beforeEach(() => {
strapi = {
query: jest.fn().mockReturnValue({
create: jest.fn().mockReturnValue({
data: {
name: 'test',
status: false,
},
}),
}),
};
});
it('persists a todo item', async () => {
const todo = await createService({ strapi }).create({ name: 'test' });
expect(strapi.query('plugin::todo.todo').create).toHaveBeenCalledTimes(1);
expect(todo.data.name).toBe('test');
});
});
通过专注于 mocking 代码接触到的特定 Strapi API,你可以扩展这些测试以覆盖更多分支、错误情况和服务,同时保持它们的快速和隔离。
🌐 By focusing on mocking the specific Strapi APIs your code touches, you can grow these tests to cover additional branches, error cases, and services while keeping them fast and isolated.
建立测试环境
🌐 Set up a testing environment
对于使用 Supertest 进行 API 级别测试,框架必须有一个干净的空环境以执行有效测试,并且不会干扰你的开发数据库。
一旦 jest 运行,它会使用 test 环境,所以请使用以下方式创建 ./config/env/test/database.js:
🌐 Once jest is running it uses the test environment, so create ./config/env/test/database.js with the following:
module.exports = ({ env }) => {
const filename = env('DATABASE_FILENAME', '.tmp/test.db');
const rawClient = env('DATABASE_CLIENT', 'sqlite');
const client = ['sqlite3', 'better-sqlite3'].includes(rawClient) ? 'sqlite' : rawClient;
return {
connection: {
client,
connection: {
filename,
},
useNullAsDefault: true,
},
};
};
此配置镜像了生产环境中使用的默认设置,但将 better-sqlite3 转换为 Strapi 所期望的 sqlite 客户端。
🌐 This configuration mirrors the defaults used in production but converts better-sqlite3 to the sqlite client Strapi expects.
Dist 目录和多个数据库配置:
在本地开发时,你可能同时拥有一个项目级的 config/database.(ts|js) 和一个环境特定的 config/env/test/database.js。
🌐 When developing locally you might have both a project-level config/database.(ts|js) and an environment-specific config/env/test/database.js.
如果你在开发环境中运行应用(例如,yarn dev),Strapi 会将配置编译到 dist/config。如果你的测试随后强制 Strapi 从 dist 读取(例如,通过传递 createStrapi({ appDir: './', distDir: './dist' })),可能最终只有一个数据库配置在 dist/config/database.js 中。这可能导致 Jest 在开发构建后获取错误的数据库设置。
🌐 If you run the app in development (e.g., yarn dev) Strapi compiles configurations into dist/config. If your tests then force Strapi to read from dist (e.g., by passing createStrapi({ appDir: './', distDir: './dist' })), only one database config may end up in dist/config/database.js. This can cause Jest to pick up the wrong DB settings after a dev build.
建议:
🌐 Recommendations:
- 不要在测试工具中传递自定义的
distDir;让 Strapi 直接从源加载。本指南中的工具调用createStrapi().load()时没有使用覆盖,这可以防止冲突。 - 在 Jest 中始终依赖
config/env/test/database.js。避免在yarn test之前立即运行yarn dev。如果已经运行过,考虑移除dist/或者干脆在不强制distDir的情况下运行测试。 - 如果必须使用
dist/,请确保其config/database.js与你的测试环境匹配,或者专门为测试进行清理/重建。
创建 Strapi 测试工具
🌐 Create the Strapi test harness
我们将在你的项目根目录中创建一个 tests 文件夹,并添加以下示例文件。这三个文件共同工作以创建一个完整的测试基础设施:
🌐 We will create a tests folder in your project root and add the example files below. These 3 files work together to create a complete testing infrastructure:
ts-compiler-options.js定义了 TypeScript 文件应如何被编译用于测试ts-runtime.js使 Jest 能够即时理解并执行 TypeScript 文件strapi.js是主要的 测试工具 用于为测试设置和拆除 Strapi 实例
TypeScript 编译器配置
🌐 TypeScript compiler configuration
使用以下内容创建 tests/ts-compiler-options.js:
🌐 Create tests/ts-compiler-options.js with the following content:
const fs = require('fs');
const path = require('path');
const ts = require('typescript');
const projectRoot = path.resolve(__dirname, '..');
const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
const baseCompilerOptions = {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2019,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
esModuleInterop: true,
jsx: ts.JsxEmit.React,
};
const loadCompilerOptions = () => {
let options = { ...baseCompilerOptions };
if (!fs.existsSync(tsconfigPath)) {
return options;
}
try {
const tsconfigContent = fs.readFileSync(tsconfigPath, 'utf8');
const parsed = ts.parseConfigFileTextToJson(tsconfigPath, tsconfigContent);
if (!parsed.error && parsed.config && parsed.config.compilerOptions) {
options = {
...options,
...parsed.config.compilerOptions,
};
}
} catch (error) {
// Ignore tsconfig parsing errors and fallback to defaults
}
return options;
};
module.exports = {
compilerOptions: loadCompilerOptions(),
loadCompilerOptions,
};
此文件加载项目的 TypeScript 配置,并在配置文件不存在时提供合理的默认值。
🌐 This file loads your project's TypeScript configuration and provides sensible defaults if the config file doesn't exist.
TypeScript 运行时加载器
🌐 TypeScript runtime loader
使用以下内容创建 tests/ts-runtime.js:
🌐 Create tests/ts-runtime.js with the following content:
const Module = require('module');
const { compilerOptions } = require('./ts-compiler-options');
const fs = require('fs');
const ts = require('typescript');
const extensions = Module._extensions;
if (!extensions['.ts']) {
extensions['.ts'] = function compileTS(module, filename) {
const source = fs.readFileSync(filename, 'utf8');
const output = ts.transpileModule(source, {
compilerOptions,
fileName: filename,
reportDiagnostics: false,
});
return module._compile(output.outputText, filename);
};
}
if (!extensions['.tsx']) {
extensions['.tsx'] = extensions['.ts'];
}
module.exports = {
compilerOptions,
};
这个文件教 Node.js 如何通过即时将 .ts 和 .tsx 文件转译为 JavaScript 来加载它们。
🌐 This file teaches Node.js how to load .ts and .tsx files by transpiling them to JavaScript on the fly.
主测试框架
🌐 Main test harness
使用以下内容创建 tests/strapi.js:
🌐 Create tests/strapi.js with the following content:
测试工具的作用:
🌐 What the test harness does:
- TypeScript 支持:修补 Strapi 的配置加载器,以便在你的配置目录中识别 TypeScript 文件(
.ts、.cts、.mts) - 配置验证:确保只加载有效的配置文件,并对常见错误发出警告(例如将文件命名为
middleware.js而不是middlewares.js) - 数据库规范化:将数据库客户端名称映射到其实际驱动程序名称(例如,
sqlite→sqlite3)并处理连接池 - 环境设置:设置测试所需的所有环境变量,包括 JWT 秘钥和数据库配置
- 自动路由注册:自动注册一个
/api/hello测试端点,你可以在测试中使用 - 用户权限助手:修补用户服务,以自动将“已认证”角色分配给新创建的用户,从而简化身份验证测试
- 清理:在测试完成后,正确关闭连接并删除临时数据库文件
tests/strapi.js 测试框架的代码示例高亮了第 313-321 行,因为这些是可选的,如果你 种子可预测的测试数据 则使用它们。
🌐 The code example for the tests/strapi.js harness highlights lines 313-321 because these are optional, to be used if you seed predictable test data.
这些文件到 位后,线束会自动处理 Strapi 5 的多项需求,让你专注于编写实际测试逻辑,而不是配置样板。
🌐 Once these files are in place, the harness handles several Strapi 5 requirements automatically, letting you focus on writing actual test logic rather than configuration boilerplate.
(可选) 生成可预测的测试数据
🌐 (optional) Seed predictable test data
一些 API 测试在预先加载已知文档集时会受益。你可以将项目的初始化数据功能作为可重用的函数暴露出来,并在环境标志下从测试框架中调用它:
🌐 Some API tests benefit from having a known set of documents preloaded. You can expose your project seeding as a reusable function and call it from the harness behind an environment flag:
-
从你的项目脚本中导出一个播种函数(例如
./scripts/seed.js):./scripts/seed.jsasync function seedExampleApp() {
// In test environment, skip complex seeding and just log
if (process.env.NODE_ENV === 'test') {
console.log('Test seeding: Skipping complex data import (not needed for basic tests)');
return;
}
const shouldImportSeedData = await isFirstRun();
if (shouldImportSeedData) {
try {
console.log('Setting up the template...');
await importSeedData();
console.log('Ready to go');
} catch (error) {
console.log('Could not import seed data');
console.error(error);
}
}
}
// Allow usage both as a CLI and as a library from tests
if (require.main === module) {
main().catch((error) => {
console.error(error);
process.exit(1);
});
}
module.exports = { seedExampleApp }; -
在测试框架中,当
TEST_SEED=true时调用该函数(参见 主测试框架 的代码示例中高亮的第 313-321 行)。 -
启用种子功能运行测试:
- Yarn
- NPM
TEST_SEED=true yarn testTEST_SEED=true npm run test
种子功能在 Strapi 启动后运行,因此服务、权限和上传功能均可用。
🌐 Seeding runs after Strapi starts, so services, permissions, and uploads are available.
建议保持种子确定性以确保断言稳定。如果发布条目,最好使用固定时间戳或对结构属性进行断言,而不是对瞬时日期进行断言。
🌐 It's recommended to keep seeds deterministic to ensure stable assertions. If you publish entries, prefer fixed timestamps or assert on structural properties rather than transient dates.
创建冒烟测试
🌐 Create smoke tests
在安装好测试环境后,你可以通过添加一个最小的 Jest 测试套件来确认 Strapi 是否正确启动,包含以下冒烟测试 在 tests/app.test.js 中如下所示:
const { setupStrapi, cleanupStrapi } = require('./strapi');
/** this code is called once before any test is called */
beforeAll(async () => {
await setupStrapi(); // Singleton so it can be called many times
});
/** this code is called once before all the tests are finished */
afterAll(async () => {
await cleanupStrapi();
});
it('strapi is defined', () => {
expect(strapi).toBeDefined();
});
require('./hello');
require('./user');
运行 yarn test 或 npm run test 现在应该会得到:
🌐 Running yarn test or npm run test should now yield:
PASS tests/create-service.test.js
PASS tests/todo-controller.test.js
Test Suites: 6 passed, 6 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 7.952 s
Ran all test suites.
✨ Done in 8.63s.
如果你在使用 Jest 时收到超时错误,可以通过在 tests/strapi.js 中或测试文件顶部调用 jest.setTimeout(30000) 来增加超时时间。
🌐 If you receive a timeout error for Jest, increase the timeout by calling jest.setTimeout(30000) in tests/strapi.js or at the top of your test file.
测试一个基本的 API 端点
🌐 Test a basic API endpoint
使用以下内容创建 tests/hello.test.js:
🌐 Create tests/hello.test.js with the following:
const { setupStrapi, cleanupStrapi } = require('./strapi');
const request = require('supertest');
beforeAll(async () => {
await setupStrapi();
});
afterAll(async () => {
await cleanupStrapi();
});
it('should return hello world', async () => {
await request(strapi.server.httpServer)
.get('/api/hello')
.expect(200)
.then((data) => {
expect(data.text).toBe('Hello World!');
});
});
该框架会自动注册 /api/hello 路由,因此测试只需发出请求即可。
🌐 The harness registers the /api/hello route automatically, so the test only has to make the request.
测试 API 身份验证
🌐 Test API authentication
Strapi 使用 JWT 令牌来处理身份验证。我们将创建一个具有已知用户名和密码的用户,并使用这些凭据进行身份验证以获取 JWT 令牌。测试工具中的修补 user.add 辅助函数确保已自动应用经过身份验证的角色。
🌐 Strapi uses a JWT token to handle authentication. We will create one user with a known username and password, and use these credentials to authenticate and get a JWT token. The patched user.add helper in the harness ensures the authenticated role is applied automatically.
创建 tests/auth.test.js:
🌐 Create tests/auth.test.js:
const { setupStrapi, cleanupStrapi } = require('./strapi');
const request = require('supertest');
beforeAll(async () => {
await setupStrapi();
});
afterAll(async () => {
await cleanupStrapi();
});
// 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)
.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();
});
});
你可以使用返回的 JWT 令牌对 API 发起经过身份验证的请求。使用此示例,你可以添加更多测试以验证身份验证和授权是否按预期工作。
🌐 You can use the JWT token returned to make authenticated requests to the API. Using this example, you can add more tests to validate that the authentication and authorization are working as expected.
具有用户权限的高级API测试
🌐 Advanced API testing with user permissions
当你创建 API 测试时,你很可能需要测试需要身份验证的端点。在以下示例中,我们将实现一个辅助工具来获取和使用 JWT 令牌。
🌐 When you create API tests, you will most likely need to test endpoints that require authentication. In the following example we will implement a helper to get and use the JWT token.
创建 tests/user.test.js:
🌐 Create tests/user.test.js:
const { setupStrapi, cleanupStrapi } = require('./strapi');
const request = require('supertest');
beforeAll(async () => {
await setupStrapi();
});
afterAll(async () => {
await cleanupStrapi();
});
let authenticatedUser = {};
// User mock data
const mockUserData = {
username: 'tester',
email: 'tester@strapi.com',
provider: 'local',
password: '1234abc',
confirmed: true,
blocked: null,
};
describe('User API', () => {
beforeAll(async () => {
await strapi.plugins['users-permissions'].services.user.add({
...mockUserData,
});
const response = await request(strapi.server.httpServer)
.post('/api/auth/local')
.set('accept', 'application/json')
.set('Content-Type', 'application/json')
.send({
identifier: mockUserData.email,
password: mockUserData.password,
});
authenticatedUser.jwt = response.body.jwt;
authenticatedUser.user = response.body.user;
});
it('should return users data for authenticated user', async () => {
await request(strapi.server.httpServer)
.get('/api/users/me')
.set('accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Authorization', 'Bearer ' + authenticatedUser.jwt)
.expect('Content-Type', /json/)
.expect(200)
.then((data) => {
expect(data.body).toBeDefined();
expect(data.body.id).toBe(authenticatedUser.user.id);
expect(data.body.username).toBe(authenticatedUser.user.username);
expect(data.body.email).toBe(authenticatedUser.user.email);
});
});
});
使用 GitHub Actions 自动化测试
🌐 Automate tests with GitHub Actions
更进一步,你可以使用 GitHub Actions在每次推送和拉取请求时自动运行你的Jest测试套件。在你的项目中创建一个.github/workflows/test.yaml文件,并按如下方式添加工作流:
name: 'Tests'
on:
pull_request:
push:
jobs:
run-tests:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install modules
run: npm ci
- name: Run Tests
run: npm run test
将持续集成与单元测试和 API 测试相结合,有助于在回归测试进入生产环境之前防止其发生。
🌐 Pairing continuous integration with your unit and API tests helps prevent regressions before they reach production.