DEV Community

Strapi
Strapi

Posted on • Originally published at strapi.io

Add Unit Tests To Your Strapi Plugin

Cameron Paczek (2)

Outline

  • Introduction
  • Prerequisites
  • What Is Jest
  • Creating Our First Test Suite
  • Creating Our First Test
  • Running Tests
  • Automatically Run Tests With GitHub Actions
  • Conclusion

Introduction

Testing is a very important part of software development. It allows you to ensure that your code is working as expected and that it will continue to work as expected in the future. In this article, we'll be looking at how to add unit tests using the Jest testing framework to your Strapi plugin.

The code for this tutorial is based on a simple todo plugin which you can find here

If you are wanting to follow along with the same example without the completed test code use this repo

Prerequisites

To follow along with this post you will need the following

  • A v4 Strapi Plugin
  • Understanding of Javascript
  • Understanding of Strapi
  • A basic understanding of Jest
  • Git and GitHub (Required for Actions)

What Is Jest

Jest is a testing framework for Javascript. It is a very popular testing framework and is used by many large companies such as Facebook, Airbnb, and Netflix. Jest is a very powerful testing framework and has many features that make it easy to write tests. Jest is also very fast and can run tests in parallel.

Unit tests are aimed at testing individual parts of your code. They are used to ensure that your code is working as expected. Unit tests are very important as they allow you to ensure that your code is working as expected and that it will continue to work as expected in the future.

Installing Jest

To install Jest, run the following command in your terminal

npm install --save-dev jest
Enter fullscreen mode Exit fullscreen mode

or

yarn add --dev jest
Enter fullscreen mode Exit fullscreen mode

Then in your package.json file add the following

{
  "scripts": {
    "test": "jest"
  }
}
Enter fullscreen mode Exit fullscreen mode

Configuring Jest

Jest has a lot of global configuration variables but for the purpose of this tutorial, we are going to leave everything as the default. If you want to learn more about how to configure Jest, you can read the Jest documentation.
First, we are going to create a new folder to store all of our tests, you should create this in the root of your application and the name is tests.

All tests are going to end with .test.js (or .test.ts). We can go ahead and create our first test file. Create a new file called tests/todo-controller.test.js and add the following code to it.

Creating Our First Test Suite

In ./server/controllers/todo-controller.js we have a very simple controller:

module.exports = ({ strapi }) => ({
  async index(ctx) {
    const { name } = ctx.request.body
    await strapi.plugin('todo').service('create').create({ name })
    return ctx.body = 'created'
  },
  async complete(ctx) {
    const { id } = ctx.request.body
    await strapi.plugin('todo').service('complete').complete({ id })
    return ctx.body = 'todo completed'
  },
})
Enter fullscreen mode Exit fullscreen mode

This controller allows us to create a todo and complete a todo. We are going to write two tests, one for each of these functions.

To create our first test suite we are going to create a new file called tests/todo-controller.test.js and add the following code to it.

let todoController = require('../server/controllers/todo-controller');

describe('Todo Controller', () => {
  it('should create a todo', async function () {
    // TODO: Write test
  })
})
Enter fullscreen mode Exit fullscreen mode

We want to import the todoController so that we can call the functions that we want to test. We then create a test suite called Todo Controller and then we create a test called should create a todo. We are going to write our test inside of the it function.

Creating Our First Test

We are going to create a test to test the index function. When making a unit test it's easiest to test pure functions (i.e functions that don't call other functions) however we have a different service that we are calling in our index function. We are going to use a mock function to mock the service that we are calling.

Mocking the Strapi Object

Because we are only testing the index function we are just going to mock any external calls. You typically want to do this for any functions that are being called.

To mock the strapi object we are going to use the jest.mock function. We are going to mock the strapi.plugin('todo').service('create').create function. We are going to mock this function so that we can test that it is being called with the correct arguments.

We don't need to mock up the entire strapi object only the parts that are being used in this function. Since we are going to have multiple tests we want to reset the strapi object every time we run a test. To do this we are going to use the beforeEach function. This function will run before every test and will reset the strapi object.

Within your describe function call beforeEach() with a async function as a callback parameter.

let todoController = require('../server/controllers/todo-controller');

describe('Todo Controller', () => {
  let strapi
  beforeEach(async function () {
    // mock this teh strapi object to allow for calling of create  await strapi.plugin('todo').service('create').create({ name })
    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('should create a todo', async function () {
    // TODO: Write test
  })
})
Enter fullscreen mode Exit fullscreen mode

As you can see we are mocking plugin(), service() and create(). We are also returning a mock object from create()

Mocking Request ctx

Now that we mocked up the strapi object we need to mock the ctx object. The ctx object is the request object that is passed into the controller. Because each test is going to have a different ctx object we are going to mock it within each test instead of mocking it globally.

Within our it() function add this code

 it('should create a todo', async function () {
    const ctx = {
      request: {
        body: {
          name: 'test'
        }
      },
      body: null
    }
  })
Enter fullscreen mode Exit fullscreen mode

This ctx object mimics the ctx object that would be passed into the controller if we were actually running strapi.

Extending Mocking to Your Code

This is a very simplistic example of mocking the strapi and ctx object but as a rule, you should look through your code and mock any external calls that you are making. This will allow you to test your code without having to worry about external calls. Remember it may also be easier to re-arrange your code so that you are testing pure functions.

Calling the Controller

Now that we have mocked the strapi and ctx objects we can call the controller. We are going to call the index() function and pass in the ctx object.

  it('should create a todo', async function () {
    const ctx = {
      request: {
        body: {
          name: 'test'
        }
      },
      body: null
    }
    // call the index function
    await todoController({strapi}).index(ctx)
    // expect the body to be 'created'
    expect(ctx.body).toBe('created')
    // expect create to be called once
    expect(strapi.plugin('todo').service('create').create).toBeCalledTimes(1)
  })
Enter fullscreen mode Exit fullscreen mode

Here you can see we are passing in our strapi object into the todoController then calling index and passing in the ctx object. We then expect the ctx.body to be created and we expect the strapi.plugin('todo').service('create').create function to be called once.

This means that if down the line the create service isn't being called or if the function is returning a different value then the test will fail.

We can do the same thing for the complete function.

  it('should complete a todo', async function () {
    const ctx = {
      request: {
        body: {
          id: 1
        }
      },
      body: null
    }
    // call the index function
    await todoController({strapi}).complete(ctx)
    // expect the body to be 'created'
    expect(ctx.body).toBe('todo completed')
    // expect create to be called once
    expect(strapi.plugin('todo').service('complete').complete).toBeCalledTimes(1)
  })
Enter fullscreen mode Exit fullscreen mode

We also need to add the complete object to our strapi object.

  beforeEach(async function () {
    // mock this teh strapi object to allow for calling of create  await strapi.plugin('todo').service('create').create({ name })
    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,
            }
          })

        })
      })
    }
  })
Enter fullscreen mode Exit fullscreen mode

Our final test suite should look like this

let todoController = require('../server/controllers/todo-controller');

describe('Todo Controller', () => {
  let strapi
  beforeEach(async function () {
    // mock this teh strapi object to allow for calling of create  await strapi.plugin('todo').service('create').create({ name })
    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('should create a todo', async function () {
    const ctx = {
      request: {
        body: {
          name: 'test'
        }
      },
      body: null
    }
    await todoController({strapi}).index(ctx)
    // expect the body to be 'created'
    expect(ctx.body).toBe('created')
    // expect create to be called once
    expect(strapi.plugin('todo').service('create').create).toBeCalledTimes(1)
  })
  it('should complete a todo', async function(){
    const ctx = {
      request: {
        body: {
          id: 1
        }
      },
      body: null
    }
    await todoController({strapi}).complete(ctx)
    // expect the body to be 'todo completed'
    expect(ctx.body).toBe('todo completed')
    // expect complete to be called once
    expect(strapi.plugin('todo').service('complete').complete).toBeCalledTimes(1)
  })
})
Enter fullscreen mode Exit fullscreen mode

Testing Services

Now that we have tested the controller we need to test the services. We are going to test the create service and you can try testing the complete service on your own.

We are going to create a new file called create-service.test.js in the tests folder.

We now need to import our create service and mock the strapi object.

//testing the create service
const createService = require('../server/services/create')
describe('Create Service', () => {
  let strapi
  beforeEach(async function () {
    strapi = {
      query: jest.fn().mockReturnValue({
        create: jest.fn().mockReturnValue({
          data: {
            name: 'test',
            status: false,
          }
        })
      })
    }
  })
  it('should create a todo', async function () {
    //todo
  })
})
Enter fullscreen mode Exit fullscreen mode

Because our create() service is calling the strapi.query() function we need to mock that function instead of the strapi.plugin() function like in our controller test suite.

Now we can build out our test, we are testing that a todo is created and is only called once and that the returned name is 'test'

  it('should create a todo', async function () {
    const name = 'test'
    const todo = await createService({ strapi }).create({ name })
    expect(strapi.query('plugin::todo.todo').create).toBeCalledTimes(1)
    expect(todo.data.name).toBe('test')
  })
Enter fullscreen mode Exit fullscreen mode

Again we pass in the mocked strapi object into the createService function and call the create() function. We then expect the strapi.query() function to be called once.

Your final service test suite should look like

//testing the create service
const createService = require('../server/services/create')
describe('Create Service', () => {
  let strapi
  beforeEach(async function () {
    strapi = {
      query: jest.fn().mockReturnValue({
        create: jest.fn().mockReturnValue({
          data: {
            name: 'test',
            status: false,
          }
        })
      })
    }
  })
  it('should create a todo', async function () {
    const name = 'test'
    const todo = await createService({ strapi }).create({ name })
    expect(strapi.query('plugin::todo.todo').create).toBeCalledTimes(1)
    expect(todo.data.name).toBe('test')
  })
})
Enter fullscreen mode Exit fullscreen mode

This is a very basic example of how you can test your controllers and services. In reality, you would be testing a lot of error logic and edge cases. If you want to see some more complex example of test check out this forms plugin

Running Tests

To run your tests you can run the following command

npm run test
Enter fullscreen mode Exit fullscreen mode

or

yarn test
Enter fullscreen mode Exit fullscreen mode

Automatically Run Tests with GitHub Actions

If you're working on a large team or you want to make sure your tests are running every time you push to GitHub you can use GitHub Actions to automatically run your tests.

To do this you need to create a new file called .github/workflows/test.yaml and add the following code

name: 'Tests'

on:
  pull_request:
  push:
jobs:
  run-tests:
    name: Run Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install modules
        working-directory: ./
        run: npm ci
      - name: Run Tests
        working-directory: ./
        run: npm run test

Enter fullscreen mode Exit fullscreen mode

This will run your tests every time you push to GitHub or create a pull request.

You can also set up branch protection rules within GitHub to prevent merging to master if your tests are failing. To do this go to your repository settings and click on the Branches tab. Then click on Add rule and select the branch you want to protect. Then select Require status checks to pass before merging. You also want to enable Require a pull request before merging and Include administrators. This will prevent you from merging to master if your tests are failing.

Conclusion

Testing can be very annoying and time-consuming but in the long run, it will save you a lot of time and headaches and maybe even stop production from going down.

Top comments (0)