Unit testing

In this guide we will see how you can run basic unit tests for a Strapi application using a testing framework.

TIP

In this example we will use JestUnit testing - 图1 (opens new window) Testing Framework with a focus on simplicity and SupertestUnit testing - 图2 (opens new window) Super-agent driven library for testing node.js HTTP servers using a fluent API

WARNING

Please note that this guide will not work if you are on Windows using the SQLite database due to how windows locks the SQLite file

Install test tools

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 allows you to test all the api routes as they were instances of http.ServerUnit testing - 图3 (opens new window)

sqlite3 is used to create an on-disk database that is created and deleted between tests.

yarn add --dev jest supertest sqlite3

npm install jest supertest sqlite3 --save-dev

Once this is done add this to package.json file

add test command to scripts section

  1. "scripts": {
  2. "develop": "strapi develop",
  3. "start": "strapi start",
  4. "build": "strapi build",
  5. "strapi": "strapi",
  6. "test": "jest --forceExit --detectOpenHandles"
  7. },

and add those line at the bottom of file

  1. "jest": {
  2. "testPathIgnorePatterns": [
  3. "/node_modules/",
  4. ".tmp",
  5. ".cache"
  6. ],
  7. "testEnvironment": "node"
  8. }

Those will inform Jest not to look for test inside the folder where it shouldn’t.

Introduction

Testing environment

Test framework must have a clean empty environment to perform valid test and also not to interfere with current database.

Once jest is running it uses the test environment (switching NODE_ENV to test) so we need to create a special environment setting for this purpose. Create a new config for test env ./config/env/test/database.json and add the following value "filename": ".tmp/test.db" - the reason of that is that we want to have a separate sqlite database for tests, so our test will not touch real data. This file will be temporary, each time test is finished, we will remove that file that every time tests are run on the clean database. The whole file will look like this:

Path — ./config/env/test/database.json

  1. {
  2. "defaultConnection": "default",
  3. "connections": {
  4. "default": {
  5. "connector": "bookshelf",
  6. "settings": {
  7. "client": "sqlite",
  8. "filename": ".tmp/test.db"
  9. },
  10. "options": {
  11. "useNullAsDefault": true,
  12. "pool": {
  13. "min": 0,
  14. "max": 1
  15. }
  16. }
  17. }
  18. }
  19. }

Strapi instance

In order to test anything we need to have a strapi instance that runs in the testing eviroment, basically we want to get instance of strapi app as object, similar like creating an instance for process manager.

These tasks require adding some files - let’s create a folder tests where all the tests will be put and inside it, next to folder helpers where main Strapi helper will be in file strapi.js.

Path — ./tests/helpers/strapi.js

  1. const Strapi = require('strapi');
  2. const http = require('http');
  3. let instance;
  4. async function setupStrapi() {
  5. if (!instance) {
  6. /** the following code in copied from `./node_modules/strapi/lib/Strapi.js` */
  7. await Strapi().load();
  8. instance = strapi; // strapi is global now
  9. await instance.app
  10. .use(instance.router.routes()) // populate KOA routes
  11. .use(instance.router.allowedMethods()); // populate KOA methods
  12. instance.server = http.createServer(instance.app.callback());
  13. }
  14. return instance;
  15. }
  16. module.exports = { setupStrapi };

Test strapi instance

We need a main entry file for our tests, one that will also test our helper file.

Path — ./tests/app.test.js

  1. const fs = require('fs');
  2. const { setupStrapi } = require('./helpers/strapi');
  3. /** this code is called once before any test is called */
  4. beforeAll(async done => {
  5. await setupStrapi(); // singleton so it can be called many times
  6. done();
  7. });
  8. /** this code is called once before all the tested are finished */
  9. afterAll(async done => {
  10. const dbSettings = strapi.config.get('database.connections.default.settings');
  11. //delete test database after all tests
  12. if (dbSettings && dbSettings.filename) {
  13. const tmpDbFile = `${__dirname}/../${dbSettings.filename}`;
  14. if (fs.existsSync(tmpDbFile)) {
  15. fs.unlinkSync(tmpDbFile);
  16. }
  17. }
  18. done();
  19. });
  20. it('strapi is defined', () => {
  21. expect(strapi).toBeDefined();
  22. });

Actually this is all we need for writing unit tests. Just run yarn test and see a result of your first test

  1. yarn run v1.13.0
  2. $ jest
  3. PASS tests/app.test.js
  4. strapi is defined (2 ms)
  5. Test Suites: 1 passed, 1 total
  6. Tests: 1 passed, 1 total
  7. Snapshots: 0 total
  8. Time: 4.187 s
  9. Ran all test suites.
  10. Done in 5.73s.

Note: if you receive a timeout error for Jest, please add the following line right before the beforeAll method in the app.test.js file: jest.setTimeout(15000) and adjust the milliseconds value as you need.

Testing basic endpoint controller.

TIP

In the example we’ll use and example Hello world /hello endpoint from controllers section.

Some might say that API tests are not unit but limited integration tests, regardless of nomenclature, let’s continue with testing first endpoint.

We’ll test if our endpoint works properly and route /hello does return Hello World

Let’s create a separate test file were supertest will be used to check if endpoint works as expected.

Path — ./tests/hello/index.js

  1. const request = require('supertest');
  2. it('should return hello world', async done => {
  3. await request(strapi.server) // app server is an instance of Class: http.Server
  4. .get('/hello')
  5. .expect(200) // Expect response http code 200
  6. .then(data => {
  7. expect(data.text).toBe('Hello World!'); // expect the response text
  8. });
  9. done();
  10. });

Then include this code to ./tests/app.test.js at the bottom of that file

  1. require('./hello');

and run yarn test which should return

  1. my-project yarn test
  2. yarn run v1.13.0
  3. $ jest --detectOpenHandles
  4. PASS tests/app.test.js (5.742 s)
  5. strapi is defined (4 ms)
  6. should return hello world (208 ms)
  7. [2020-05-22T14:37:38.018Z] debug GET /hello (58 ms) 200
  8. Test Suites: 1 passed, 1 total
  9. Tests: 2 passed, 2 total
  10. Snapshots: 0 total
  11. Time: 6.635 s, estimated 7 s
  12. Ran all test suites.
  13. Done in 9.09s.

Testing auth endpoint controller.

In this scenario we’ll test authentication login endpoint with two tests

  1. Test /auth/local that should login user and return jwt token
  2. Test /users/me that should return users data based on Authorization header

Path — ./tests/user/index.js

  1. const request = require('supertest');
  2. // user mock data
  3. const mockUserData = {
  4. username: 'tester',
  5. email: 'tester@strapi.com',
  6. provider: 'local',
  7. password: '1234abc',
  8. confirmed: true,
  9. blocked: null,
  10. };
  11. it('should login user and return jwt token', async done => {
  12. /** Creates a new user and save it to the database */
  13. await strapi.plugins['users-permissions'].services.user.add({
  14. ...mockUserData,
  15. });
  16. await request(strapi.server) // app server is an instance of Class: http.Server
  17. .post('/auth/local')
  18. .set('accept', 'application/json')
  19. .set('Content-Type', 'application/json')
  20. .send({
  21. identifier: mockUserData.email,
  22. password: mockUserData.password,
  23. })
  24. .expect('Content-Type', /json/)
  25. .expect(200)
  26. .then(data => {
  27. expect(data.body.jwt).toBeDefined();
  28. });
  29. done();
  30. });
  31. it('should return users data for authenticated user', async done => {
  32. /** Gets the default user role */
  33. const defaultRole = await strapi.query('role', 'users-permissions').findOne({}, []);
  34. const role = defaultRole ? defaultRole.id : null;
  35. /** Creates a new user an push to database */
  36. const user = await strapi.plugins['users-permissions'].services.user.add({
  37. ...mockUserData,
  38. username: 'tester2',
  39. email: 'tester2@strapi.com',
  40. role,
  41. });
  42. const jwt = strapi.plugins['users-permissions'].services.jwt.issue({
  43. id: user.id,
  44. });
  45. await request(strapi.server) // app server is an instance of Class: http.Server
  46. .get('/users/me')
  47. .set('accept', 'application/json')
  48. .set('Content-Type', 'application/json')
  49. .set('Authorization', 'Bearer ' + jwt)
  50. .expect('Content-Type', /json/)
  51. .expect(200)
  52. .then(data => {
  53. expect(data.body).toBeDefined();
  54. expect(data.body.id).toBe(user.id);
  55. expect(data.body.username).toBe(user.username);
  56. expect(data.body.email).toBe(user.email);
  57. });
  58. done();
  59. });

Then include this code to ./tests/app.test.js at the bottom of that file

  1. require('./user');

All the tests above should return an console output like

  1. my-project git:(master) yarn test
  2. yarn run v1.13.0
  3. $ jest --forceExit --detectOpenHandles
  4. [2020-05-27T08:30:30.811Z] debug GET /hello (10 ms) 200
  5. [2020-05-27T08:30:31.864Z] debug POST /auth/local (891 ms) 200
  6. PASS tests/app.test.js (6.811 s)
  7. strapi is defined (3 ms)
  8. should return hello world (54 ms)
  9. should login user and return jwt token (1049 ms)
  10. should return users data for authenticated user (163 ms)
  11. Test Suites: 1 passed, 1 total
  12. Tests: 4 passed, 4 total
  13. Snapshots: 0 total
  14. Time: 6.874 s, estimated 9 s
  15. Ran all test suites.
  16. Done in 8.40s.