# Unit and Route Testing
Testing code units and API routes in a Strapi application can be done with Jest (opens new window) and Supertest (opens new window), with an SQLite database. This documentation describes how to:
- write a unit test for a function,
- test a public API endpoint test,
- test and an API endpoint test with authorization.
Refer to the Jest testing framework documentation for other use cases and for the full set of testing options and configurations.
✋ CAUTION
The tests described below are incompatible with Windows using the SQLite database due to how Windows locks the SQLite file.
# Install and configure the test tools
The following section briefly describes each of the required tools and the installation procedure.
Jest
contains a set of guidelines or rules used for creating and designing test cases - a combination of practices and tools that are designed to help testers test more efficiently.
Supertest
provides high-level abstraction for testing HTTP requests and responses. It still allows you to test the API routes.
better-sqlite3
is used to create an on-disk database that is created before and deleted after the tests run.
# Install for JavaScript applications
- Add the tools to the dev dependencies:
- Add
test
to thepackage.json
filescripts
section:
"scripts": {
"develop": "strapi develop",
"start": "strapi start",
"build": "strapi build",
"strapi": "strapi",
"test": "jest --forceExit --detectOpenHandles --watchAll"
},
2
3
4
5
6
7
- Add a
jest
section to thepackage.json
file with the following code:
//path: ./package.json
//...
"jest": {
"testPathIgnorePatterns": [
"/node_modules/",
".tmp",
".cache"
],
"testEnvironment": "node"
}
//...
2
3
4
5
6
7
8
9
10
11
- Save and close your
package.json
file.
# Create a testing environment
The testing environment should test the application code without affecting the database, and should be able to run distinct units of the application to incrementally test the code functionality. To achieve this the following procedure adds:
- a test database configuration,
- a
strapi
instance for testing, - file directories to organize the testing environment.
# Create a test environment database configuration file
The test framework must have a clean and empty environment to perform valid tests and to not interfere with the development database. Once jest
is running it uses the test
environment by switching NODE_ENV
to test
.
- Create the subdirectories
env/test/
in the./config/
directory. - Create a new database configuration file
database.js
for the test environment in./config/env/test/
. - Add the following code to
./config/env/test/database.js
:
// path: ./config/env/test/database.js
const path = require('path');
module.exports = ({ env }) => ({
connection: {
client: 'sqlite',
connection: {
filename: path.join(__dirname, '..', env('DATABASE_FILENAME', '.tmp/data.db')),
},
useNullAsDefault: true,
},
});
2
3
4
5
6
7
8
9
10
11
12
13
# Create a strapi
instance
The testing environment requires a strapi
instance as an object, similar to creating an instance for the process manager.
- Create a
tests
directory at the application root, which hosts all of the tests. - Create a
helpers
directory insidetests
, which hosts thestrapi
instance and other supporting functions. - Create the file
strapi.js
in thehelpers
directory and add the following code:
//path: ./tests/helpers/strapi.js
const Strapi = require("@strapi/strapi");
const fs = require("fs");
let instance;
async function setupStrapi() {
if (!instance) {
await Strapi().load();
instance = strapi;
await instance.server.mount();
}
return instance;
}
async function teardownStrapi() {
const dbSettings = strapi.config.get("database.connection");
//close server to release the db-file
await strapi.server.httpServer.close();
// close the connection to the database before deletion
await strapi.db.connection.destroy();
//delete test database after all tests have completed
if (dbSettings && dbSettings.connection && dbSettings.connection.filename) {
const tmpDbFile = dbSettings.connection.filename;
if (fs.existsSync(tmpDbFile)) {
fs.unlinkSync(tmpDbFile);
}
}
}
module.exports = { setupStrapi, teardownStrapi };
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
✏️ NOTE
The command to close the database connection does not always work correctly, which results in an open handle warning in Jest
. The --watchAll
flag temporarily solves this problem.
# Test the strapi
instance
You need a main entry file for tests, which can also be used to test the strapi
instance.
- Create
app.test.js
in thetests
directory. - Add the following code to
app.test.js
:
//path: ./tests/app.test.js
const fs = require('fs');
const { setupStrapi, teardownStrapi } = require("./helpers/strapi");
beforeAll(async () => {
await setupStrapi();
});
afterAll(async () => {
await teardownStrapi();
});
it("strapi is defined", () => {
expect(strapi).toBeDefined(); //confirms that the strapi instance is defined
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- Run the test to confirm it is working correctly:
- Confirm the test is working. The test output in the terminal window should be the following: <!--update this with the --watchAll flag-->
yarn run v1.22.18
$ jest --forceExit --detectOpenHandles
PASS tests/app.test.js
✓ strapi is defined (1 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.043 s, estimated 3 s
Ran all test suites.
✨ Done in 2.90s.
2
3
4
5
6
7
8
9
10
11
✏️ NOTE
Jest detects test files by looking for the filename
{your-file}.test.js
.If you receive a timeout error for Jest, please add the following line before the
beforeAll
method in theapp.test.js
file:jest.setTimeout(15000)
and adjust the milliseconds value as necessary.
# Run a unit test
Unit tests are designed to test individual units such as functions and methods. The following procedure sets up a unit test for a function to demonstrate the functionality:
Create the file
sum.js
at the application root.Add the following code to the
sum.js
file:// path: ./sum.js function sum(a, b) { return a + b; } module.exports = sum;
1
2
3
4
5
6
7
8Add the location of the code to be tested to the
app.test.js
file:// path: ./tests/app.test.js //... const sum = require('../sum'); //...
1
2
3
4
5
6Add the test criteria to the
app.test.js
file:
// path: ./tests/app.test.js
//...
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
//...
2
3
4
5
6
7
8
- Save the files and run
yarn test
ornpm test
in the project root directory. Jest should return a test summary that confirms the test suite and test were successful.
# Test public endpoints
The goal of this test is to evaluate if an endpoint works properly. In this example both the route and controller logic have to work for the test to be successful. This example uses a custom route and controller, but the same structure works with APIs generated using the Content-type Builder.
# Create a public route and controller
Routes direct incoming requests to the server while controllers contain the business logic. For this example, the route authorizes GET
for /public
and calls the hello
method in the public
controller.
Add a directory
public
to./src/api
.Add sub directories
routes
andcontrollers
inside the new./src/api/public
directory.Create a
public.js
file inside theroutes
directory and add the following code:// path: ./src/api/public/routes/public.js module.exports = { routes: [ { method: 'GET', path: '/public', handler: 'public.hello', config: { auth: false, // enables the public route }, }, ], };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Create a
public.js
file inside thecontrollers
directory and add the following code:// path: ./src/api/public/controllers/public.js module.exports = { async hello(ctx, next) { // called by GET /public ctx.body = 'Hello World!'; }, };
1
2
3
4
5
6
7
8
9Save both of the
public.js
files.
# Create a public endpoint test
An endpoint test has 3 components:
the strapi instance created in the create a strapi instance section,
a
public.js
file in the./tests
directory that contains the testing criteria,and a modified
app.test.js
file created in Test the strapi instance that contains theJest
test functions.
Create a test file
public.js
in./tests
.Add the following code to
public.js
:// path: ./tests/public.js const request = require('supertest'); it("should return some text here", async () => { await request(strapi.server.httpServer) .get("/api/public") //add your API route here .expect(200) // Expect response http code 200 .then((data) => { expect(data.text).toBe("Hello World!"); // expect the response text }); });
1
2
3
4
5
6
7
8
9
10
11
12
13💡 TIP
You can add the test logic directly to the
app.test.js
file, however, if you write a lot of tests, using separate files for the test logic can be useful.Add the following code to
./tests/app.test.js
//... require('./public'); //...
1
2
3Save your code changes.
Run
yarn test
ornpm test
to confirm the test is successful.
# Test an authenticated API endpoint
PREREQUISITES
The authenticated API endpoint test utilizes the strapi.js
helper file created in the Create a strapi
instance documentation.
In order to test API endpoints that require authentication you must create a mock user as part of the test setup. In the following example the Users and Permissions plugin is used to create a mock user. The mock user is stored in the testing database and deleted at the end of the test. Testing authenticated API endpoints requires:
- modifying the
strapi.js
testing instance, - creating a
user.js
helper file to mock a user, - writing the authenticated route test.
- running the test.
# Modify strapi.js
testing instance
To enable authenticated route tests the strapi.js
helper file needs to issue a JWT and set the permissions for the mock user. Add the following code to your strapi.js
helper file:
Add
lodash
belowfs
://const Strapi = require("@strapi/strapi"); //const fs = require("fs"); const _ = require("lodash");
1
2
3Add the following code to the bottom of the
strapi.js
helper file:/** * Returns valid JWT token for authenticated * @param {String | number} idOrEmail, either user id, or email */ const jwt = (idOrEmail) => strapi.plugins["users-permissions"].services.jwt.issue({ [Number.isInteger(idOrEmail) ? "id" : "email"]: idOrEmail, }); /** * Grants database `permissions` table that role can access an endpoint/controllers * * @param {int} roleID, 1 Authenticated, 2 Public, etc * @param {string} value, in form or dot string eg `"permissions.users-permissions.controllers.auth.changepassword"` * @param {boolean} enabled, default true * @param {string} policy, default '' */ const grantPrivilege = async ( roleID = 1, path, enabled = true, policy = "" ) => { const service = strapi.plugin("users-permissions").service("role"); const role = await service.findOne(roleID); _.set(role.permissions, path, { enabled, policy }); return service.updateRole(roleID, role); }; /** Updates database `permissions` so that role can access an endpoint * @see grantPrivilege */ const grantPrivileges = async (roleID = 1, values = []) => { await Promise.all(values.map((val) => grantPrivilege(roleID, val))); }; /** * Updates the core of strapi * @param {*} pluginName * @param {*} key * @param {*} newValues * @param {*} environment */ const updatePluginStore = async ( pluginName, key, newValues, environment = "" ) => { const pluginStore = strapi.store({ environment: environment, type: "plugin", name: pluginName, }); const oldValues = await pluginStore.get({ key }); const newValue = Object.assign({}, oldValues, newValues); return pluginStore.set({ key: key, value: newValue }); }; /** * Get plugin settings from store * @param {*} pluginName * @param {*} key * @param {*} environment */ const getPluginStore = (pluginName, key, environment = "") => { const pluginStore = strapi.store({ environment: environment, type: "plugin", name: pluginName, }); return pluginStore.get({ key }); }; /** * Check if response error contains error with given ID * @param {string} errorId ID of given error * @param {object} response Response object from strapi controller * @example * * const response = { data: null, error: { status: 400, name: 'ApplicationError', message: 'Your account email is not confirmed', details: {} } } * responseHasError("ApplicationError", response) // true */ const responseHasError = (errorId, response) => { return response && response.error && response.error.name === errorId; };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105Add the following to
module.exports
:module.exports{ setupStrapi, teardownStrapi, jwt, grantPrivilege, grantPrivileges, updatePluginStore, getPluginStore, responseHasError, };
1
2
3
4
5
6
7
8
9
10
# Create a user
helper file
A user
helper file is used to create a mock user account in the test database. This code can be reused for other tests that also need user credentials to login or test other functionalities. To setup the user
helper file:
Create
user.js
in the./tests/helpers
directory.Add the following code to the
user.js
file:/** * Default data that factory use */ const defaultData = { password: "1234Abc", provider: "local", confirmed: true, }; /** * Returns random username object for user creation * @param {object} options that overwrites default options * @returns {object} object used with strapi.plugins["users-permissions"].services.user.add */ const mockUserData = (options = {}) => { const usernameSuffix = Math.round(Math.random() * 10000).toString(); return { username: `tester${usernameSuffix}`, email: `tester${usernameSuffix}@strapi.com`, ...defaultData, ...options, }; }; /** * Creates new user in strapi database * @param data * @returns {object} object of new created user, fetched from database */ const createUser = async (data) => { /** Gets the default user role */ const pluginStore = await strapi.store({ type: "plugin", name: "users-permissions", }); const settings = await pluginStore.get({ key: "advanced", }); const defaultRole = await strapi .query("plugin::users-permissions.role") .findOne({ where: { type: settings.default_role } }); /** Creates a new user and push to database */ return strapi .plugin("users-permissions") .service("user") .add({ ...mockUserData(), ...data, role: defaultRole ? defaultRole.id : null, }); }; module.exports = { mockUserData, createUser, defaultData, };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62Save the file.
# Create an auth.test.js
test file
The auth.test.js
file contains the authenticated endpoint test conditions.
code:
const { describe, beforeAll, afterAll, it, expect } = require("@jest/globals");
const request = require("supertest");
const {
updatePluginStore,
responseHasError,
setupStrapi,
stopStrapi,
} = require("./helpers/strapi");
const { createUser, defaultData, mockUserData } = require("./helpers/user");
const fs = require("fs");
beforeAll(async () => {
await setupStrapi();
});
afterAll(async () => {
await stopStrapi();
});
describe("Default User methods", () => {
let user;
beforeAll(async () => {
user = await createUser();
});
it("should login user and return jwt token", async () => {
const jwt = strapi.plugins["users-permissions"].services.jwt.issue({
id: user.id,
});
await request(strapi.server.httpServer)
.post("/api/auth/local")
.set("accept", "application/json")
.set("Content-Type", "application/json")
.send({
identifier: user.email,
password: defaultData.password,
})
.expect("Content-Type", /json/)
.expect(200)
.then(async (data) => {
expect(data.body.jwt).toBeDefined();
const verified = await strapi.plugins[
"users-permissions"
].services.jwt.verify(data.body.jwt);
expect(data.body.jwt === jwt || !!verified).toBe(true);
});
});
it("should return user's data for authenticated user", async () => {
const jwt = strapi.plugins["users-permissions"].services.jwt.issue({
id: user.id,
});
await request(strapi.server.httpServer)
.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);
});
});
it("should allow register users ", async () => {
await request(strapi.server.httpServer)
.post("/api/auth/local/register")
.set("accept", "application/json")
.set("Content-Type", "application/json")
.send({
...mockUserData(),
})
.expect("Content-Type", /json/)
.expect(200)
.then((data) => {
expect(data.body).toBeDefined();
expect(data.body.jwt).toBeDefined();
expect(data.body.user).toBeDefined();
});
});
});
describe("Confirmation User methods", () => {
let user;
beforeAll(async () => {
await updatePluginStore("users-permissions", "advanced", {
email_confirmation: true,
});
user = await createUser({
confirmed: false,
});
});
afterAll(async () => {
await updatePluginStore("users-permissions", "advanced", {
email_confirmation: false,
});
});
it("unconfirmed user should not login", async () => {
await request(strapi.server.httpServer)
.post("/api/auth/local")
.set("accept", "application/json")
.set("Content-Type", "application/json")
.send({
identifier: user.email,
password: defaultData.password,
})
.expect("Content-Type", /json/)
.expect(400)
.then((data) => {
expect(responseHasError("ApplicationError", data.body)).toBe(true);
});
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# Run an authenticated API endpoint test
The above test is designed to:
- login an authenticated user and return a
jwt
, - return the user's data,
- allow the registration of users,
- dissallow unauthenticated users to access the endpoint.
Use the following command to run the authenticated test: