单元和集成测试指南
🌐 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.