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
/helloroute and authenticated role during setup.
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.
The guide aims to recreate the minimal test suite available in the strapi-unit-testing-examples CodeSandbox link.
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
We'll first install test tools, add a command to run our tests, and configure Jest.
-
Install Jest and Supertest by running the following command in a terminal:
- Yarn
- NPM
yarn add jest supertest --devnpm install jest supertest --save-devJestprovides the test runner and assertion utilities.Supertestallows you to test all theapiroutes as they were instances of http.Server.
-
Update the
package.jsonfile of your Strapi project with the following:-
Add a
testcommand to thescriptssection 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"
}
}
-
Mock Strapi for plugin unit tests
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 utilities to recreate just the parts of the Strapi object and any request context that your code relies on.
Controller example
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);
});
});
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
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');
});
});
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
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.
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,
},
};
};
This configuration mirrors the defaults used in production but converts better-sqlite3 to the sqlite client Strapi expects.
Dist directory and multiple database configurations:
When developing locally you might have both a project-level config/database.(ts|js) and an environment-specific config/env/test/database.js.
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:
- Do not pass a custom
distDirin the test harness; let Strapi load directly from source. The harness in this guide callscreateStrapi().load()without overrides, which prevents the conflict. - Always rely on
config/env/test/database.jsfor Jest. Avoid runningyarn devimmediately beforeyarn test. If you did, consider removingdist/or simply run tests without forcingdistDir. - If you must use
dist/, ensure itsconfig/database.jsmatches your test environment, or clean/rebuild specifically for tests.
Create the Strapi test harness
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.jsdefines how TypeScript files should be compiled for testingts-runtime.jsenables Jest to understand and execute TypeScript files on the flystrapi.jsis the main test harness that sets up and tears down Strapi instances for tests
TypeScript compiler configuration
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,
};
This file loads your project's TypeScript configuration and provides sensible defaults if the config file doesn't exist.