Skip to main content

单元和集成测试指南

¥Unit and integration testing guide

Page summary:

Testing relies on Jest and Supertest with an in-memory SQLite database, a patched Strapi test harness that also supports TypeScript configuration files, and helpers that automatically register the /hello route and authenticated role during setup.

本指南提供了在 Strapi 5 应用中配置 Jest 的实践方法,包括模拟 Strapi 对象以进行单元测试插件代码,以及使用 超测 端到端测试 REST 端点。

¥The present guide provides a hands-on approach to configuring Jest in a Strapi 5 application, mocking the Strapi object for unit testing plugin code, and using Supertest to test REST endpoints end to end.

本指南旨在重新创建 strapi-unit-testing-examples CodeSandbox 链接中提供的最小测试套件。

¥The guide aims to recreate the minimal test suite available in the strapi-unit-testing-examples CodeSandbox link.

提醒

如果你在 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.

  1. 通过在终端中运行以下命令安装 Jest 和 Supertest:

    ¥Install Jest and Supertest by running the following command in a terminal:

```bash
yarn add jest supertest --dev
```


* `Jest` provides the test runner and assertion utilities.

* `Supertest` allows you to test all the `api` routes as they were instances of <ExternalLink to="https://nodejs.cn/api/http.html#class-httpserver" text="http.Server"/>.

* Add a `test` command to the `scripts` section so it looks as follows:


        "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"
},

* Configure Jest at the bottom of the file to ignore Strapi build artifacts and to map any root-level modules you import from tests:


        "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

¥Pure unit tests are ideal for Strapi plugins because they let you validate controller and service logic without starting a Strapi server. Use Jest's mocking

用于重新创建 Strapi 对象的各个部分以及代码所依赖的任何请求上下文的实用程序。

¥utilities to recreate just the parts of the Strapi object and any request context that your code relies on.

控制器示例

¥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:

./tests/todo-controller.test.js


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 钩子会重建模拟,因此每个测试都从一个干净的 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.

./tests/create-service.test.js


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

对于使用 超测 进行 API 级测试,框架必须具有干净的空环境才能执行有效测试,并且不会干扰你的开发数据库。

¥For API-level testing with Supertest , the framework must have a clean empty environment to perform valid tests and also not to interfere with your development database.

jest 运行时会使用 test environment,因此请使用以下命令创建 ./config/env/test/database.js

¥Once jest is running it uses the test environment, so create ./config/env/test/database.js with the following:

./config/env/test/database.js
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 directory and multiple database configurations:

在本地开发时,你可能同时拥有项目级 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() 时无需覆盖,从而避免了冲突。

    ¥Do not pass a custom distDir in the test harness; let Strapi load directly from source. The harness in this guide calls createStrapi().load() without overrides, which prevents the conflict.

  • Jest 始终依赖 config/env/test/database.js。避免在 yarn test 之前立即运行 yarn dev。如果存在这种情况,请考虑移除 dist/ 或直接运行测试而不强制使用 distDir

    ¥Always rely on config/env/test/database.js for Jest. Avoid running yarn dev immediately before yarn test. If you did, consider removing dist/ or simply run tests without forcing distDir.

  • 如果你必须使用 dist/,请确保其 config/database.js 与你的测试环境匹配,或者专门为测试清理/重建。

    ¥If you must use dist/, ensure its config/database.js matches your test environment, or clean/rebuild specifically for tests.

创建 Strapi 测试工具

¥Create the Strapi test harness

我们将在项目根目录中创建一个 tests 文件夹,并添加以下示例文件。这 3 个文件协同工作,以创建一个完整的测试基础架构:

¥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-compiler-options.js defines how TypeScript files should be compiled for testing

  • ts-runtime.js 使 Jest 能够动态理解和执行 TypeScript 文件。

    ¥ts-runtime.js enables Jest to understand and execute TypeScript files on the fly

  • strapi.js 是主要的测试工具。

    ¥strapi.js is the main test harness

用于设置和拆除 Strapi 实例以进行测试

¥that sets up and tears down Strapi instances for tests

TypeScript 编译器配置

¥TypeScript compiler configuration

创建包含以下内容的 tests/ts-compiler-options.js

¥Create tests/ts-compiler-options.js with the following content:

./tests/ts-compiler-options.js


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:

./tests/ts-runtime.js


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:

./tests/strapi.js
try {
require('ts-node/register/transpile-only');
} catch (err) {
try {
require('@strapi/typescript-utils/register');
} catch (strapiRegisterError) {
require('./ts-runtime');
}
}



const fs = require('fs');




const path = require('path');




const Module = require('module');




const ts = require('typescript');




const databaseConnection = require('@strapi/database/dist/connection.js');




const knexFactory = require('knex');




const strapiCoreRoot = path.dirname(require.resolve('@strapi/core/package.json'));




const loadConfigFilePath = path.join(strapiCoreRoot, 'dist', 'utils', 'load-config-file.js');




const loadConfigFileModule = require(loadConfigFilePath);


const { compilerOptions: baseCompilerOptions } = require('./ts-compiler-options');

// ============================================
// 1. PATCH: TypeScript Configuration Loader
// ============================================
// This section patches Strapi's configuration loader to support TypeScript config files
// (.ts, .cts, .mts). Without this, Strapi would only load .js and .json config files.

if (!loadConfigFileModule.loadConfigFile.__tsRuntimePatched) {
const strapiUtils = require('@strapi/utils');
const originalLoadConfigFile = loadConfigFileModule.loadConfigFile;

const loadTypeScriptConfig = (file) => {
const source = fs.readFileSync(file, 'utf8');
const options = {
...baseCompilerOptions,
module: ts.ModuleKind.CommonJS,
};

const output = ts.transpileModule(source, {
compilerOptions: options,
fileName: file,
reportDiagnostics: false,
});

const moduleInstance = new Module(file);
moduleInstance.filename = file;
moduleInstance.paths = Module._nodeModulePaths(path.dirname(file));
moduleInstance._compile(output.outputText, file);

const exported = moduleInstance.exports;
const resolved = exported && exported.__esModule ? exported.default : exported;

if (typeof resolved === 'function') {
return resolved({ env: strapiUtils.env });
}

return resolved;
};

const patchedLoadConfigFile = (file) => {
const extension = path.extname(file).toLowerCase();

if (extension === '.ts' || extension === '.cts' || extension === '.mts') {
return loadTypeScriptConfig(file);
}

return originalLoadConfigFile(file);
};

patchedLoadConfigFile.__tsRuntimePatched = true;
loadConfigFileModule.loadConfigFile = patchedLoadConfigFile;
require.cache[loadConfigFilePath].exports = loadConfigFileModule;
}

// ============================================
// 2. PATCH: Configuration Directory Scanner
// ============================================
// This section patches how Strapi scans the config directory to:
// - Support TypeScript extensions (.ts, .cts, .mts)
// - Validate config file names
// - Prevent loading of restricted filenames



const configLoaderPath = path.join(strapiCoreRoot, 'dist', 'configuration', 'config-loader.js');




const originalLoadConfigDir = require(configLoaderPath);




const validExtensions = ['.js', '.json', '.ts', '.cts', '.mts'];




const mistakenFilenames = {


middleware: 'middlewares',
plugin: 'plugins',
};


const restrictedFilenames = [


'uuid',
'hosting',
'license',
'enforce',
'disable',
'enable',
'telemetry',
'strapi',
'internal',
'launchedAt',
'serveAdminPanel',
'autoReload',
'environment',
'packageJsonStrapi',
'info',
'dirs',
...Object.keys(mistakenFilenames),
];


const strapiConfigFilenames = ['admin', 'server', 'api', 'database', 'middlewares', 'plugins', 'features'];



if (!originalLoadConfigDir.__tsRuntimePatched) {
const patchedLoadConfigDir = (dir) => {
if (!fs.existsSync(dir)) {
return {};
}

const entries = fs.readdirSync(dir, { withFileTypes: true });
const seenFilenames = new Set();

const configFiles = entries.reduce((acc, entry) => {
if (!entry.isFile()) {
return acc;
}

const extension = path.extname(entry.name);
const extensionLower = extension.toLowerCase();
const baseName = path.basename(entry.name, extension);
const baseNameLower = baseName.toLowerCase();

if (!validExtensions.includes(extensionLower)) {
console.warn(`Config file not loaded, extension must be one of ${validExtensions.join(',')}): ${entry.name}`);
return acc;
}

if (restrictedFilenames.includes(baseNameLower)) {
console.warn(`Config file not loaded, restricted filename: ${entry.name}`);
if (baseNameLower in mistakenFilenames) {
console.log(`Did you mean ${mistakenFilenames[baseNameLower]}?`);
}
return acc;
}

const restrictedPrefix = [...restrictedFilenames, ...strapiConfigFilenames].find(
(restrictedName) => restrictedName.startsWith(baseNameLower) && restrictedName !== baseNameLower
);

if (restrictedPrefix) {
console.warn(`Config file not loaded, filename cannot start with ${restrictedPrefix}: ${entry.name}`);
return acc;
}

if (seenFilenames.has(baseNameLower)) {
console.warn(`Config file not loaded, case-insensitive name matches other config file: ${entry.name}`);
return acc;
}

seenFilenames.add(baseNameLower);
acc.push(entry);
return acc;
}, []);

return configFiles.reduce((acc, entry) => {
const extension = path.extname(entry.name);
const key = path.basename(entry.name, extension);
const filePath = path.resolve(dir, entry.name);

acc[key] = loadConfigFileModule.loadConfigFile(filePath);
return acc;
}, {});
};

patchedLoadConfigDir.__tsRuntimePatched = true;
require.cache[configLoaderPath].exports = patchedLoadConfigDir;
}

// ============================================
// 3. PATCH: Database Connection Handler
// ============================================
// This section normalizes database client names for testing.
// Maps Strapi's client names (sqlite, mysql, postgres) to actual driver names
// (sqlite3, mysql2, pg) and handles connection pooling.

databaseConnection.createConnection = (() => {
const clientMap = {
sqlite: 'sqlite3',
mysql: 'mysql2',
postgres: 'pg',
};

return (userConfig, strapiConfig) => {
if (!clientMap[userConfig.client]) {
throw new Error(`Unsupported database client ${userConfig.client}`);
}

const knexConfig = {
...userConfig,
client: clientMap[userConfig.client],
};

if (strapiConfig?.pool?.afterCreate) {
knexConfig.pool = knexConfig.pool || {};

const userAfterCreate = knexConfig.pool?.afterCreate;
const strapiAfterCreate = strapiConfig.pool.afterCreate;

knexConfig.pool.afterCreate = (conn, done) => {
strapiAfterCreate(conn, (err, nativeConn) => {
if (err) {
return done(err, nativeConn);
}

if (userAfterCreate) {
return userAfterCreate(nativeConn, done);
}

return done(null, nativeConn);
});
};
}

return knexFactory(knexConfig);
};
})();

// ============================================
// 4. TEST ENVIRONMENT SETUP
// ============================================
// Configure Jest timeout and set required environment variables for testing

if (typeof jest !== 'undefined' && typeof jest.setTimeout === 'function') {
jest.setTimeout(30000);
}

const { createStrapi } = require('@strapi/strapi');

process.env.NODE_ENV = process.env.NODE_ENV || 'test';
process.env.APP_KEYS = process.env.APP_KEYS || 'testKeyOne,testKeyTwo';
process.env.API_TOKEN_SALT = process.env.API_TOKEN_SALT || 'test-api-token-salt';
process.env.ADMIN_JWT_SECRET = process.env.ADMIN_JWT_SECRET || 'test-admin-jwt-secret';
process.env.TRANSFER_TOKEN_SALT = process.env.TRANSFER_TOKEN_SALT || 'test-transfer-token-salt';
process.env.ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef';
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret';
process.env.DATABASE_CLIENT = process.env.DATABASE_CLIENT || 'sqlite';
process.env.DATABASE_FILENAME = process.env.DATABASE_FILENAME || ':memory:';
process.env.STRAPI_DISABLE_CRON = 'true';
process.env.PORT = process.env.PORT || '0';



const databaseClient = process.env.DATABASE_CLIENT || 'sqlite';




const clientMap = {


sqlite: 'sqlite3',
'better-sqlite3': 'sqlite3',
mysql: 'mysql2',
postgres: 'pg',
};



const driver = clientMap[databaseClient];



if (!driver) {
throw new Error(`Unsupported database client "${databaseClient}".`);
}

if (databaseClient === 'better-sqlite3') {
process.env.DATABASE_CLIENT = 'sqlite';
}

require(driver);

let instance;

// ============================================
// 5. STRAPI INSTANCE MANAGEMENT
// ============================================
// Functions to set up and tear down a Strapi instance for testing

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

// Register the /api/hello test route automatically
const contentApi = instance.server?.api?.('content-api');
if (contentApi && !instance.__helloRouteRegistered) {
const createHelloService = require(path.join(
__dirname,
'..',
'src',
'api',
'hello',
'services',
'hello'
));
const helloService = createHelloService({ strapi: instance });

contentApi.routes([
{
method: 'GET',
path: '/hello',
handler: async (ctx) => {
ctx.body = await helloService.getMessage();
},
config: {
auth: false,
},
},
]);

contentApi.mount(instance.server.router);
instance.__helloRouteRegistered = true;
}

await instance.start();
global.strapi = instance;

// Optionally seed example data for tests if requested
if (process.env.TEST_SEED === 'true') {
try {
const { seedExampleApp } = require(path.join(__dirname, '..', 'scripts', 'seed'));
await seedExampleApp();
} catch (e) {
console.warn('Seeding failed:', e);
}
}

// Patch the user service to automatically assign the authenticated role
const userService = strapi.plugins['users-permissions']?.services?.user;
if (userService) {
const originalAdd = userService.add.bind(userService);

userService.add = async (values) => {
const data = { ...values };

if (!data.role) {
const defaultRole = await strapi.db
.query('plugin::users-permissions.role')
.findOne({ where: { type: 'authenticated' } });

if (defaultRole) {
data.role = defaultRole.id;
}
}

return originalAdd(data);
};
}
}
return instance;
}

async function cleanupStrapi() {
if (!global.strapi) {
return;
}

const dbSettings = strapi.config.get('database.connection');

await strapi.server.httpServer.close();
await strapi.db.connection.destroy();

if (typeof strapi.destroy === 'function') {
await strapi.destroy();
}

if (dbSettings && dbSettings.connection && dbSettings.connection.filename) {
const tmpDbFile = dbSettings.connection.filename;
if (fs.existsSync(tmpDbFile)) {
fs.unlinkSync(tmpDbFile);
}
}
}

module.exports = { setupStrapi, cleanupStrapi };

测试工具的作用:

¥What the test harness does:

  1. TypeScript 支持:修补 Strapi 的配置加载器,使其能够理解配置目录中的 TypeScript 文件(.ts.cts.mts)。

    ¥TypeScript Support: Patches Strapi's configuration loader to understand TypeScript files (.ts, .cts, .mts) in your config directory

  2. 配置验证:确保仅加载有效的配置文件,并警告常见错误(例如将文件命名为 middleware.js 而不是 middlewares.js)。

    ¥Configuration Validation: Ensures only valid config files are loaded and warns about common mistakes (like naming a file middleware.js instead of middlewares.js)

  3. 数据库规范化:将数据库客户端名称映射到其实际驱动程序名称(例如,sqlitesqlite3)并处理连接池

    ¥Database Normalization: Maps database client names to their actual driver names (e.g., sqlitesqlite3) and handles connection pooling

  4. 环境设置:设置测试所需的所有环境变量,包括 JWT 密钥和数据库配置

    ¥Environment Setup: Sets all required environment variables for testing, including JWT secrets and database configuration

  5. 自动路由注册:自动注册一个 /api/hello 测试端点,你可以在测试中使用。

    ¥Automatic Route Registration: Automatically registers a /api/hello test endpoint that you can use in your tests

  6. 用户权限助手:修补用户服务,使其自动将 "authenticated" 角色分配给新创建的用户,从而简化身份验证测试。

    ¥User Permission Helper: Patches the user service to automatically assign the "authenticated" role to newly created users, simplifying authentication tests

  7. 清理:测试完成后,正确关闭连接并删除临时数据库文件。

    ¥Cleanup: Properly closes connections and removes temporary database files after tests complete

注意

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:

  1. 从项目脚本中导出种子函数(例如 ./scripts/seed.js):

    ¥Export a seeding function from your project script (e.g. ./scripts/seed.js):

    ./scripts/seed.js
    async 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 };
  2. 在测试工具中,在 TEST_SEED=true 时调用该函数(参见 主要测试工具 代码示例中高亮的第 313-321 行)。

    ¥In the test harness, call the function when TEST_SEED=true (see lines 313-321 highlighted in the code example from the main test harness).

  3. 启用种子功能运行测试:

    ¥Run your tests with seeding enabled:

  ```bash
TEST_SEED=true yarn 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 是否正确启动。

¥With the harness in place you can confirm Strapi boots correctly by adding a minimal Jest suite with the following smoke tests

tests/app.test.js 中如下所示:

¥in a tests/app.test.js as follows:

./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 testnpm 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:

./tests/hello.test.js
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:

./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:

./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 文件,并添加如下工作流:

¥To go further, you can run your Jest test suite automatically on every push and pull request with GitHub Actions. Create a .github/workflows/test.yaml file in your project and add the workflow as follows:

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