# 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

  1. Add the tools to the dev dependencies:
  1. Add test to the package.json file scripts section:





 


  "scripts": {
    "develop": "strapi develop",
    "start": "strapi start",
    "build": "strapi build",
    "strapi": "strapi",
    "test": "jest --forceExit --detectOpenHandles --watchAll"
  },
1
2
3
4
5
6
7
  1. Add a jest section to the package.json file with the following code:
//path: ./package.json
//...
  "jest": {
    "testPathIgnorePatterns": [
      "/node_modules/",
      ".tmp",
      ".cache"
    ],
    "testEnvironment": "node"
  }
  //...
1
2
3
4
5
6
7
8
9
10
11
  1. 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.

  1. Create the subdirectories env/test/ in the ./config/ directory.
  2. Create a new database configuration file database.js for the test environment in ./config/env/test/.
  3. 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,
  },
});
1
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.

  1. Create a tests directory at the application root, which hosts all of the tests.
  2. Create a helpers directory inside tests, which hosts the strapi instance and other supporting functions.
  3. Create the file strapi.js in the helpers 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 };
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

✏️ 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.

  1. Create app.test.js in the tests directory.
  2. 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
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. Run the test to confirm it is working correctly:
  1. 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.
1
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 the app.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:

  1. Create the file sum.js at the application root.

  2. 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
    8
  3. Add 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
    6
  4. Add 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);
    });
    //...

1
2
3
4
5
6
7
8
  1. Save the files and run yarn test or npm 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.

  1. Add a directory public to ./src/api.

  2. Add sub directories routes and controllers inside the new ./src/api/public directory.

  3. Create a public.js file inside the routes 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
    16
  4. Create a public.js file inside the controllers 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
    9
  5. Save 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 the Jest test functions.

  1. Create a test file public.js in ./tests.

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

  3. Add the following code to ./tests/app.test.js

    //...
    require('./public');
    //...
    
    1
    2
    3
  4. Save your code changes.

  5. Run yarn test or npm 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:

  1. Add lodash below fs:

    //const Strapi = require("@strapi/strapi");
    //const fs = require("fs");
    const _ = require("lodash");
    
    1
    2
    3
  2. Add 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
    105
  3. Add 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:

  1. Create user.js in the ./tests/helpers directory.

  2. 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
    62
  3. Save 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);
      });
  });
});
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
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: