Creating Automated Tests for Strapi API with PactumJS

strapijs

Strapi

Posted on February 17, 2023

Creating Automated Tests for Strapi API with PactumJS

Automated tests can be essential to ensure that your application is running smoothly and free of bugs. You can catch potential issues early on in the development process. This step-by-step tutorial will teach you how to create automated tests for Strapi API using PactumJS.

You will use Strapi to build the back-end of a simple To-Do list application. Then set up PactumJS, a testing framework to create automated tests for our back-end. We will learn the basics of Strapi while creating the back-end, dive into the basics of automated testing using PactumJS and learn about making different requests and validating the responses.

This tutorial does not cover all the details of Strapi or PactumJS framework, it’s a guide to get you started and take the first steps.

Prerequisites

Before you can jump into this content, you need to have a basic understanding of the following.

  • Basic JavaScript knowledge
  • NPM installed
  • Basic knowledge of Strapi and RESTful APIs

What is Strapi?

Strapi is an open-source headless CMS (Content Management System) that allows you to quickly create and maintain RESTful JavaScript APIs. Strapi helps create simple and complex back-ends, either as an individual or an organization. Strapi is built on NodeJS, which provides high performance in processing large amounts of requests simultaneously.

Setting up Strapi Locally

We will create a new Strapi application that will provide us with an admin dashboard that allows us to create and handle the back-end operations, including the database schema, the API endpoints, and the records in the database. We can create a Strapi application using npm or yarn using the following commands:

  • npm
    npx create-strapi-app todo-list --quickstart
Enter fullscreen mode Exit fullscreen mode
  • yarn
    yarn install global create-strapi-app
    yarn create-strapi-app todo-list --quickstart
Enter fullscreen mode Exit fullscreen mode
  • yarn v3 or above
    yarn dlx create-strapi-app todo-list --quickstart
Enter fullscreen mode Exit fullscreen mode

After creating the Strapi application successfully, we will run the application in the development environment, which will create a local server for us by default to allow us to create endpoints, data collections, and set up authentication for our back-end and handle it through the admin dashboard.

To run the Strapi application in development mode, navigate to the project folder, fire up your favorite terminal, and run the following commands according to your package manager:

  • npm
    npm run develop 
Enter fullscreen mode Exit fullscreen mode
  • yarn
    yarn run develop
Enter fullscreen mode Exit fullscreen mode

Then open http://localhost:1337/admin on your browser. The application should be loaded on a registration page that looks like this:

Strapi Admin Panel

On the welcome page, we will create your admin account, which we will use to access the Strapi admin dashboard. Only this user will have access to the Strapi dashboard until you create other users with the same privileges using the admin account. When we create the admin account and hit "Let’s start". We will get directed to the dashboard that contains all the possible options for us to build our back-end in the left panel.

Strapi CMS Dashboard Page

Building a To-do List Back-end

To learn how to build a full To-do List application using Strapi and React, I recommend reading this article How to Build a To-Do List Application with Strapi and ReactJS, which goes into more details in explaining how to build a CMS using Strapi.

We will build a simple to-do list back-end, which will contain a few endpoints attached to data collections stored in our database, Strapi will handle most of the work for us. We will just define what we need, which are:

  • To-do collection
    • contains one field called item of type text.
  • A few test entries in the Todo collection
  • API that performs CRUD operations on the Todo collection

What will Strapi take care of:

  • Handle creating the actual endpoints and the code that performs each CRUD operation. We will just enable them for certain visitors (in our case, everyone - public)
  • Handle creating and connecting to the actual database, which is SQLite by default.
  • Handle creating the database schema, inserting, retrieving, altering, and deleting the data with only a few buttons or an API call from us.

So, let’s see how we can define these needs.

Creating a To-do Collection

A data collection in Strapi is a group of data representing something or an entity in your application; for example, if you create an online clothing store, you would create a collection called "Item", that holds the information of each item you have in store. The information could be, for example, id, type, color, available sizes, available, and so on. This information is called attributes, they define the item you have.

Let’s create our first data collection, “To-do Collection”:

  1. Navigate to “Content-Type Builder” under “plugins” at the left. This will open the app to the page that enables us to manage our data collection.
  2. Click “Create new collection type” to create a new collection.
  3. Type “Todo” (you can name it whatever you want) in the display name field. This will be the collection name.
  4. Click Continue.
  5. Click on the “Text” button to add a text field to your To-do collection.
  6. Type “task” (you can name it whatever you want) in the name field. This will be the name of the single attribute we need in the data collection.
  7. Select “long-text” because a Todo can be as long as it needs. We don’t want to limit it to a short text.
  8. Click “Finish”.
  9. Click on “save” to register the collection in our application. Note: Registering a collection makes the server restart.

Adding Test Entries to the To-do Collection

The test entries will be the data in the to-do collection, the records themselves. We need a few meaningless entries to test the collection because it’s an empty collection now.

  1. Navigate to “Content-Manager”. This will take us to where we can manipulate the actual data in the collections.
  2. Click on Todo at the left to display the current entries, which are empty “No content found”.
  3. Click on “Create new entry”
  4. You will find the attribute above, the “task” attribute, now type in any meaningless words, “task A” for example.
  5. Hit “Save” to save the entry as a draft.
  6. Hit “Publish” to register the entry in the collection and make it visible to the API we will enable in the next step.
  7. Repeat the steps twice to have at least three tasks in our To-do collection.

Creating an API Endpoint for our Collection

Creating API endpoints enables us to build up our back-end and use it by calling these endpoints and using the data by performing CRUD operations on our collections from the front-end for example, in our case, we will call the API from our testing script.

To create endpoints, follow the following steps:

  1. Navigate to “Settings” under “general”.
  2. Click “Roles” under “user permission & roles”.
  3. Click on “public” to open the permissions given to the public.
  4. Toggle the “To-do” drop-down under “Permissions”. This controls public access to the “To-do” collection.
  5. Click on “Select all” to allow public access to the collection without authentication through the endpoints.
  6. Hit “Save”.

The following endpoints will be created for each of the permission we enabled. Let’s try to request each endpoint and take a look at the request and response.

  • Find - GET /api/todos/
    Gets all the todos in our Todo collection. If we call it from a browser or an HTTP client, we will get a JSON object containing all the todos in our collection.

  • Response

    {
            "data": [
                {
                    "id": 1,
                    "attributes": {
                        "task": "task A",
                        "createdAt": "2022-12-19T10:33:44.577Z",
                        "updatedAt": "2022-12-19T10:33:45.723Z",
                        "publishedAt": "2022-12-19T10:33:45.718Z"
                    }
                },
                {
                    "id": 2,
                    "attributes": {
                        "task": "task B",
                        "createdAt": "2022-12-19T10:33:56.381Z",
                        "updatedAt": "2022-12-19T10:33:58.147Z",
                        "publishedAt": "2022-12-19T10:33:58.144Z"
                    }
                }
            ],
            "meta": {
                "pagination": {
                    "page": 1,
                    "pageSize": 25,
                    "pageCount": 1,
                    "total": 2
                }
            }
        }
Enter fullscreen mode Exit fullscreen mode

Notice the additional data like id, createdAt, updatedAt, and publishedAt. Those are metadata Strapi injects in the database by default.

  • Create - POST /api/todos
    Creates a new todo with the data specified in the POST request, saves the new entry into the database, and publishes it in the API.

  • Request

    {
                "data": {
                        "task": "task C"
                }
    }

Enter fullscreen mode Exit fullscreen mode
  • Response
    {
            "data": {
                "id": 3,
                "attributes": {
                    "task": "task C",
                    "createdAt": "2022-12-19T10:17:36.082Z",
                    "updatedAt": "2022-12-19T10:17:36.082Z",
                    "publishedAt": "2022-12-19T10:17:36.079Z"
                }
            },
            "meta": {}
        }

Enter fullscreen mode Exit fullscreen mode
  • Find One - GET /api/todos/{id}
    Gets one entry with the specific id supplied in the URL if it exists. for example, if we requested - /api/todos/1 -. We will get a single entry of the todo with id 1.

  • Response

    {
            "data": {
                "id": 1,
                "attributes": {
                    "task": "task A",
                    "createdAt": "2022-04-19T13:15:10.869Z",
                    "updatedAt": "2022-04-19T13:15:11.839Z",
                    "publishedAt": "2022-04-19T13:15:11.836Z"
                }
            },
            "meta": {}
        }

Enter fullscreen mode Exit fullscreen mode
  • Update - PUT /api/todos/{id}
    Updates a certain entry with the id supplied in the URL to the new data specified in the request. For example, let's update the second task. We send a PUT request for /api/todos/2

  • Request

    {
                "data": {
                        "task": "task B - updated"
                }
    }
Enter fullscreen mode Exit fullscreen mode
  • Response
    {
            "data": {
                "id": 3,
                "attributes": {
                    "task": "task B - updated",
                    "createdAt": "2022-12-19T10:17:36.082Z",
                    "updatedAt": "2022-12-19T10:17:36.082Z",
                    "publishedAt": "2022-12-19T10:17:36.079Z"
                }
            },
            "meta": {}
        }
Enter fullscreen mode Exit fullscreen mode
  • Delete - DELETE /api/todos/{id}
    Deletes a certain entry with the id supplied in the URL to the new data specified in the request. For example, let's delete the third task we just created in the Create example. We send a DELETE request for /api/todos/3 we should get the task data in the response if the deletion operation was successful.

  • Response

    {
            "data": {
                "id": 2,
                "attributes": {
                    "task": "task - C",
                    "createdAt": "2022-12-19T13:17:36.082Z",
                    "updatedAt": "2022-12-19T13:15:11.839Z",
                    "publishedAt": "2022-12-19T13:15:11.836Z"
                }
            },
            "meta": {}
        }
Enter fullscreen mode Exit fullscreen mode

Now that we have the API ready, let’s dig into the testing framework.

What is PactumJS?

According to PactumJS Documentation it’s a next-generation free and open-source REST API automation testing tool for all levels in a Test Pyramid. It makes back-end testing a productive and enjoyable experience. This library provides all the necessary ingredients for the most common things to write better API automation tests in an easy, fast & fun way. In simple words, it’s an easy web testing framework that enables automated testing via writing JEST scripts that have many use cases.

Use Cases for PactumJS

  • API Testing
  • Component Testing
  • Contract Testing
  • Integration Testing
  • E2E Testing
  • Mock Server

Setting Up PactumJS

PactumJS is built on top of NodeJS, so we need NPM installed to run and install it. Fire up your terminal and type in the following commands to install PactumJS and a test runner called Mocha.

    mkdir todo-test
    cd todo-test

    # install pactum as a dev dependency
    npm install --save-dev pactum 

    # install a test runner to run pactum tests
    # (mocha) / jest / cucumber
    npm install --save-dev mocha
Enter fullscreen mode Exit fullscreen mode

After installing the dependencies, create a new file in todo-test/tests directory. We can call it test.js, for example, then update the scripts in package.json file to use mocha when we run the test script.

    {
      "scripts": 
        {
        "test": "mocha tests"
        }
    }
Enter fullscreen mode Exit fullscreen mode

While specifying the script command mocha tests you can either specify a directory path like tests here which will run mocha for each file in the directory or specify a js file like tests/test.js which then will run mocha for that specific file only.

Make sure your Strapi application is up and running. The default URL for Strapi is http://localhost:1337. So, we will import the required module and set the base URL first thing in test.js file.

    const { spec, request } = require('pactum');

    /* Set base url to the backend URL */
    request.setBaseUrl('<http://localhost:1337>')
Enter fullscreen mode Exit fullscreen mode

Now that everything is ready, we can start working on our first test!

Hello World in PactumJS

To write a new test, you need the following:

  • describe block which you will describe what you are testing for documentation and clearance. You can define which endpoint you’ll test in this block, for example.
  • one or more it blocks inside the describe block, which will have the actual API call and response validation code.
  • you need the endpoint route, but not the full route because we already specified a base URL. Now we need the route after the base only. For example, we will use /api/todos to get all the to-dos. This route will get appended to the base URL to be http://localhost:1337/api/todos.

Let’s write our first test. We will write a simple test that will hit the endpoint at /api/todos and validates that the status code in the response is 200 or OK.

First, let’s take a general look at the status codes and their meanings. Status codes are a variable returned with any response that implies the status of the request we sent, each code has a different meaning, but in general, they’re classified like this according to MDN web docs.

  1. Informational responses (100199)
  2. Successful responses (200299)
  3. Redirection messages (300399)
  4. Client error responses (400499)
  5. Server error responses (500599)

Now, let’s write the script.

    const { spec, request } = require('pactum');

    /* Set base url to the backend URL */
    request.setBaseUrl('<http://localhost:1337>')

    describe('GET Todos Tests - Retrieve all todos' , () =>
    {
        it('Should return 200 - Validate hitting get all todos endpoint', async() => 
        {
            await spec()
                .get('/api/todos')
                .expectStatus(200)
        })
    })
Enter fullscreen mode Exit fullscreen mode

Let’s dig into this described block, then run it and look at the output.

  • describe('GET Todos Tests - Retrieve all todos' , () => The describe block takes a string that defines the block and a function that may contain multiple it blocks.

  • it('Should return 200 - Validate hitting get all todos endpoint', async() => The it block also takes a string and an Async function, it’s essential to be async here because we will make an API call and we need that call to happen on a separate thread. this block will test if the status code of hitting the **/api/todos** endpoint is 200, which implies the success of the request.

  • await spec().get('/api/todos').expectStatus(200)

    • spec() exposes all methods offered by PactumJS to construct a request.
    • .get() the request type, which in our case is a regular GET request.
    • .expectStatus(200) the response validation, we’re expecting a status of 200. Think of it as an if condition, if the status is 200 then the test passes, if not it fails!

Let’s run our test against our API and see the result! To run the script, type the following command in the terminal.

    npm run test
    > mocha tests

      GET Todos Tests - Retrieve all todos
        ✔ Should return 200 - Validate hitting get all todos endpoint (82ms)

      1 passing (89ms)
Enter fullscreen mode Exit fullscreen mode

YAY! Our test passed!

Let’s dig deeper and try other request types and ways of validating responses!

Making Requests and Validating Responses

GET Request with Parameters

Let’s say we want to test the Find One endpoint, we need to supply a parameter (which is the Id in our case) in the URL. We can put it directly in the URL like this **/api/todos/1**, or we can write it in a more elegant way using a PactumJS function called withPathParams, let’s write the test block and validate that the status code is 200, and the JSON object in the response is not empty! For that, we will require notNull from a module called pactum-matchers that contains different matching functions (you can read about them in the api docs).

Let’s also add multiple it blocks to see what the output will look like.

    const { spec, request } = require('pactum');
    const { notNull } = require('pactum-matchers');

    describe('GET Todo Tests - Retrieve a signle todo by id', () =>
    {
        it('Should return 200 - Validate retrieving a single todo', async() => 
        {

            await spec()
                .get('/api/todos/{id}')
                .withPathParams('id', 1)
                .expectStatus(200)
        })

        it('Should not be empty - Validating returning a single non-empty todo', async() => 
        { 
            await spec()
                .get('/api/todos/{id}')
                .withPathParams('id', 2)
                .expectStatus(200)
                .expectJsonMatch(
                    {
                        "data": notNull()
                    });
        })
    })
Enter fullscreen mode Exit fullscreen mode

So, what’s new here?

  • .withPathParams('id', 1) takes a string and matches it to a string in the route inside a curly bracket {id} and replaces it with a value (the second parameter).
  • .expectJsonMatch( { "data": notNull() } ); A new response validation function. This function takes a JSON object as a parameter and matches it with the response. Also we can inject special functions in the object like notNull(), which means it doesn’t matter what is the value of “data”, it just has to be not null! There are other matches like this, including any(), regex(), uuid(), string().

Let’s run the tests and see if it passes. Remember that we have 2 describe blocks now and the second one has 2 it blocks

    > mocha tests

      GET Todos Tests - Retrieve all todos
        ✔ Should return 200 - Validate hitting get all todos endpoint

      GET Todo Tests - Retrieve a signle todo by id
        ✔ Should return 200 - Validate retrieving a single todo
        ✔ Should not be empty - Validating returning a single non-empty todo

      3 passing (69ms)

Enter fullscreen mode Exit fullscreen mode

POST Request

Let’s test the Create endpoint and create a new todo, and chain it with a retrieval test to make sure it got stored in the database. We need to make a POST request, and send a JSON object with the request, then retrieve the id of the newly created task, store it and make a GET request to the Find One endpoint, to validate that the task got created successfully.

    describe('POST Todo Tests - Create todo', () =>
    {
                    const taskDescription = 'Newly created task';
        it('Should return 200 - Create a new todo and validate it exists', async() => 
        {
            const id = await spec()
                .post('/api/todos')
                .withJson(
                    {
                        'data':
                        { 
                            'task': taskDescription
                        }
                    }
                )
                .expectStatus(200)
                .returns('data.id')

                await spec()
                .get('/api/todos/{id}')
                .withPathParams('id', id)
                .expectStatus(200)
                .expectJson('data.attributes.task', taskDescription);
        })

    })
Enter fullscreen mode Exit fullscreen mode
  • .withJson( { 'data': { 'task': taskDescription } } ) Takes a JSON object as a parameter, and sends it with the post request to the endpoint, Here we sent an object that holds a task, with the text “Newly created task".
  • .returns('data.id') We need the id of the newly created entry, so we use returns()function that takes the specific object inside the JSON response and returns back, which we will store in const id to use in the next part.
  • The next part is a regular GET test that asks to retrieve a single todo with the recently acquired id, and expects a 200 status code, and expects the task attribute in the JSON data response to be exactly the same string sent in the previous POST request .expectJson('data.attributes.task', taskDescription);

Let’s see if our tests pass.

    > mocha tests
      GET Todos Tests - Retrieve all todos
        ✔ Should return 200 - Validate hitting get all todos endpoint

      GET Todo Tests - Retrieve todo
        ✔ Should return 200 - Validate retrieving a single todo
        ✔ Should not be empty - Validating returning a single non-empty todo

      POST Todo Tests - Create todo
        ✔ Should return 200 - Create a new todo and validate it exists (247ms)

      4 passing (652ms)
Enter fullscreen mode Exit fullscreen mode

Great! Let’s move on.

DELETE Request

Let’s create a new task, then delete it, then confirm it got deleted by trying to retrieve it and expect a not found status code!

We will reuse the previous code of the post request again and add the DELETE request in the middle.

    describe('DELETE Todo Tests - Delete todo', () =>
    {
        const taskDescription = 'Newly created task - 2';
        it('Should return 200 - Create a new todo and validate it exists', async() => 
        { 
            const id = await spec()
                .post('/api/todos')
                .withJson(
                    {
                        'data':
                        {
                            'item': taskDescription
                        }
                    }
                )
                .expectStatus(200)
                .returns('data.id');

                await spec()
                .delete('/api/todos/{id}')
                .withPathParams('id', id)
                .expectStatus(200);

                await spec()
                .get('/api/todos/{id}')
                .withPathParams('id', id)
                .expectStatus(404);
        })

    })
Enter fullscreen mode Exit fullscreen mode

We added .delete('/api/todos/{id}') request to delete the recently created task, then a .get('/api/todos/{id}') request that expects a 404 status code (not found) to validate the entry got deleted, which means if the response status code is 404, the test will pass, if not; it will fail.

Let’s run and see if our tests pass.

    > mocha tests

      GET Todos Tests - Retrieve all todos
        ✔ Should return 200 - Validate hitting get all todos endpoint

      GET Todo Tests - Retrieve todo
        ✔ Should return 200 - Validate retrieving a single todo
        ✔ Should not be empty - Validating returning a single non-empty todo

      POST Todo Tests - Create todo
        ✔ Should return 200 - Create a new todo and validate it exists (192ms)

      DELETE Todo Tests - Delete todo
        ✔ Should return 200 - Create a new todo and validate it exists (573ms)

      5 passing (831ms)
Enter fullscreen mode Exit fullscreen mode

Looks like all of our tests pass, great!

Response Validation Techniques

We’ve learned how to send different types of requests, pass parameters with the requests, validate the response status code, and match the JSON. PactumJS has a lot more to offer. In this section, we’ll discuss some of these methods to validate the API response. Response validation ensures that the API response to a certain request is valid and correct!

Basic Assertions Techniques

Assertions are a way to make sure that the response (or part of it) matches a certain pattern or rule. There are a lot of assertion functions offered by PactumJS, we will discuss some of them, but you can find the full list documented at PactumJS API Documentation - Assertions.

  • expectStatus(code) We’ve used expectStatus earlier. It asserts that the status code of the response is a specific code.
  • expectHeaderContains(key, value) It asserts that a specific header key in the response is present and equals a specific value. for example, if we want to make sure that the response content-type is Json:
    await spec() 
            .get('/api/todos/')
            .expectHeaderContains('content-type', 'application/json');
Enter fullscreen mode Exit fullscreen mode
  • expectBodyContains(string) Performs partial equal between the supplied string and the response body and passes if it exists. For example, if the response status code is 200, the body will contain ‘OK’, So, we can check if the response body has ‘OK’:
    await spec() 
            .get('/api/todos/')
            .expectBodyContains('OK');
Enter fullscreen mode Exit fullscreen mode
  • expectResponseTime(milliseconds) Passes if the response time is less than the specified milliseconds. Example:
    await spec()
      .get('/api/todos')
      .expectResponseTime(100);
Enter fullscreen mode Exit fullscreen mode
  • expectJsonMatch([path], json) Passes if the specified JSON object matches the JSON response. We can also specify a certain JSON path to match with. For example, if we have a first name variable in the JSON object and it should be ‘Ahmed’, we can check this by:
    await spec() 
      .get('/api/users/1')
      .expectJsonMatch('data.first_name', 'Ahmed');
Enter fullscreen mode Exit fullscreen mode

Basic Matching Techniques

Most of the time, we don’t want to match specific strings in the response as we did in the previous example. We want to assert certain types (string, int, float), that the value is not empty (using notNull that we used above), that the value in a certain range (lt, lte, gt, gte), or one of several options (oneOf). These are some of the matching techniques PactumJS provides. Let’s take a look at some of them.

  • string(), int(), float() Matches the data type, here’s an example:
    await spec()
      .get('/api/users/1')
      .expectJsonMatch('data.first_name', string()) 
            .expectJsonMatch('data.id', int())
            .expectJsonMatch('data.salary', float());
Enter fullscreen mode Exit fullscreen mode
  • oneOf([]) Matches one of the values specified in the array supplied as the parameter. Here’s an example:
    await spec()
      .get('<https://randomuser.me/api>')
      .expectJsonMatch({
        "results": 
        [
          {
            "gender": oneOf(["male", "female"]),
          }
        ]
      });
Enter fullscreen mode Exit fullscreen mode

You can see how this can be handy when having fields that carry enum values.

    await spec()
      .get('<https://randomuser.me/api>')
      .expectJsonMatch({
        "results": 
        [
          {
            "login": 
            {
              "uuid": uuid()
            }
          }
        ]
      });
Enter fullscreen mode Exit fullscreen mode
  • lt(), lte(), gt(), gte() Performs integer comparison! Let’s see an example:
    await spec()
      .get('<https://randomuser.me/api>')
      .expectJsonMatch({
        "results": 
        [
          {
            "dob": 
            {
              "age": lt(100), 
              "children" : lte(2),
              "salary" : gte(4000)
            }
          }
        ]
      });
Enter fullscreen mode Exit fullscreen mode

notEquals() Checks if the actual value is not equal to the expected one, and comes in handy when testing for wrong inputs! Example:

    await spec()
      .get('<https://randomuser.me/api>')
      .expectJsonMatch({
        "results": [
          {
            "name": notEquals('jon'),
          }
        ]
      });
Enter fullscreen mode Exit fullscreen mode

There are many more matching functions to make your life easier, which you can find at PactumJS API Documentation - Matching.

Conclusion

In this tutorial, we learned:

  • How to build a simple backend using Strapi.
  • How to access the API created by Strapi.
  • How to send different API request types using PactumJS.
  • How to validate API responses using PactumJS.
  • How to use Assertions to test different parts of the response.
  • How to use Matching techniques to test specific fields.
  • That PactumJS has more and more potential that we can take advantage of once we understand the basics.

If you're interested in discussing this topic further or connecting with more people using Strapi, join our Discord community. It is a great place to share your thoughts, ask questions, and participate in live discussions.

If you're interested in discussing this topic further or connecting with more people using Strapi, join our Discord community. It is a great place to share your thoughts, ask questions, and participate in live discussions.

Strapi Enterprise Edition

💖 💪 🙅 🚩
strapijs
Strapi

Posted on February 17, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related