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.
Before you can jump into this content, you need to have a basic understanding of the following.
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.
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:
npx create-strapi-app todo-list --quickstart
yarn install global create-strapi-app
yarn create-strapi-app todo-list --quickstart
yarn dlx create-strapi-app todo-list --quickstart
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 run develop
yarn
yarn run develop
Then open http://localhost:1337/admin on your browser. The application should be loaded on a registration page that looks like this:
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.
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:
What will Strapi take care of:
So, let’s see how we can define these needs.
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”:
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.
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.
1 {
2 "data": [
3 {
4 "id": 1,
5 "attributes": {
6 "task": "task A",
7 "createdAt": "2022-12-19T10:33:44.577Z",
8 "updatedAt": "2022-12-19T10:33:45.723Z",
9 "publishedAt": "2022-12-19T10:33:45.718Z"
10 }
11 },
12 {
13 "id": 2,
14 "attributes": {
15 "task": "task B",
16 "createdAt": "2022-12-19T10:33:56.381Z",
17 "updatedAt": "2022-12-19T10:33:58.147Z",
18 "publishedAt": "2022-12-19T10:33:58.144Z"
19 }
20 }
21 ],
22 "meta": {
23 "pagination": {
24 "page": 1,
25 "pageSize": 25,
26 "pageCount": 1,
27 "total": 2
28 }
29 }
30 }
Notice the additional data like id, createdAt, updatedAt, and publishedAt. Those are metadata Strapi injects in the database by default.
1 {
2 "data": {
3 "task": "task C"
4 }
5 }
1 {
2 "data": {
3 "id": 3,
4 "attributes": {
5 "task": "task C",
6 "createdAt": "2022-12-19T10:17:36.082Z",
7 "updatedAt": "2022-12-19T10:17:36.082Z",
8 "publishedAt": "2022-12-19T10:17:36.079Z"
9 }
10 },
11 "meta": {}
12 }
1 {
2 "data": {
3 "id": 1,
4 "attributes": {
5 "task": "task A",
6 "createdAt": "2022-04-19T13:15:10.869Z",
7 "updatedAt": "2022-04-19T13:15:11.839Z",
8 "publishedAt": "2022-04-19T13:15:11.836Z"
9 }
10 },
11 "meta": {}
12 }
1 {
2 "data": {
3 "task": "task B - updated"
4 }
5 }
1 {
2 "data": {
3 "id": 3,
4 "attributes": {
5 "task": "task B - updated",
6 "createdAt": "2022-12-19T10:17:36.082Z",
7 "updatedAt": "2022-12-19T10:17:36.082Z",
8 "publishedAt": "2022-12-19T10:17:36.079Z"
9 }
10 },
11 "meta": {}
12 }
1 {
2 "data": {
3 "id": 2,
4 "attributes": {
5 "task": "task - C",
6 "createdAt": "2022-12-19T13:17:36.082Z",
7 "updatedAt": "2022-12-19T13:15:11.839Z",
8 "publishedAt": "2022-12-19T13:15:11.836Z"
9 }
10 },
11 "meta": {}
12 }
Now that we have the API ready, let’s dig into the testing framework.
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.
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
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"
}
}
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>')
Now that everything is ready, we can start working on our first test!
To write a new test, you need the following:
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.
100
– 199
)200
– 299
)300
– 399
)400
– 499
)500
– 599
)Now, let’s write the script.
1 const { spec, request } = require('pactum');
2
3 /* Set base url to the backend URL */
4 request.setBaseUrl('<http://localhost:1337>')
5
6 describe('GET Todos Tests - Retrieve all todos' , () =>
7 {
8 it('Should return 200 - Validate hitting get all todos endpoint', async() =>
9 {
10 await spec()
11 .get('/api/todos')
12 .expectStatus(200)
13 })
14 })
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)
YAY! Our test passed!
Let’s dig deeper and try other request types and ways of validating responses!
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.
1 const { spec, request } = require('pactum');
2 const { notNull } = require('pactum-matchers');
3
4 describe('GET Todo Tests - Retrieve a signle todo by id', () =>
5 {
6 it('Should return 200 - Validate retrieving a single todo', async() =>
7 {
8
9 await spec()
10 .get('/api/todos/{id}')
11 .withPathParams('id', 1)
12 .expectStatus(200)
13 })
14
15 it('Should not be empty - Validating returning a single non-empty todo', async() =>
16 {
17 await spec()
18 .get('/api/todos/{id}')
19 .withPathParams('id', 2)
20 .expectStatus(200)
21 .expectJsonMatch(
22 {
23 "data": notNull()
24 });
25 })
26 })
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)
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.
1 describe('POST Todo Tests - Create todo', () =>
2 {
3 const taskDescription = 'Newly created task';
4 it('Should return 200 - Create a new todo and validate it exists', async() =>
5 {
6 const id = await spec()
7 .post('/api/todos')
8 .withJson(
9 {
10 'data':
11 {
12 'task': taskDescription
13 }
14 }
15 )
16 .expectStatus(200)
17 .returns('data.id')
18
19 await spec()
20 .get('/api/todos/{id}')
21 .withPathParams('id', id)
22 .expectStatus(200)
23 .expectJson('data.attributes.task', taskDescription);
24 })
25
26 })
.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..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)
Great! Let’s move on.
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.
1 describe('DELETE Todo Tests - Delete todo', () =>
2 {
3 const taskDescription = 'Newly created task - 2';
4 it('Should return 200 - Create a new todo and validate it exists', async() =>
5 {
6 const id = await spec()
7 .post('/api/todos')
8 .withJson(
9 {
10 'data':
11 {
12 'item': taskDescription
13 }
14 }
15 )
16 .expectStatus(200)
17 .returns('data.id');
18
19 await spec()
20 .delete('/api/todos/{id}')
21 .withPathParams('id', id)
22 .expectStatus(200);
23
24 await spec()
25 .get('/api/todos/{id}')
26 .withPathParams('id', id)
27 .expectStatus(404);
28 })
29
30 })
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)
Looks like all of our tests pass, great!
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!
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:1 await spec()
2 .get('/api/todos/')
3 .expectHeaderContains('content-type', 'application/json');
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’:1 await spec()
2 .get('/api/todos/')
3 .expectBodyContains('OK');
expectResponseTime(milliseconds)
Passes if the response time is less than the specified milliseconds. Example:1 await spec()
2 .get('/api/todos')
3 .expectResponseTime(100);
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:1 await spec()
2 .get('/api/users/1')
3 .expectJsonMatch('data.first_name', 'Ahmed');
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:
1 await spec()
2 .get('/api/users/1')
3 .expectJsonMatch('data.first_name', string())
4 .expectJsonMatch('data.id', int())
5 .expectJsonMatch('data.salary', float());
oneOf([])
Matches one of the values specified in the array supplied as the parameter. Here’s an example:1 await spec()
2 .get('<https://randomuser.me/api>')
3 .expectJsonMatch({
4 "results":
5 [
6 {
7 "gender": oneOf(["male", "female"]),
8 }
9 ]
10 });
You can see how this can be handy when having fields that carry enum values.
uuid()
Matches a UUID format, which is the Universal Unique Identifier.1 await spec()
2 .get('<https://randomuser.me/api>')
3 .expectJsonMatch({
4 "results":
5 [
6 {
7 "login":
8 {
9 "uuid": uuid()
10 }
11 }
12 ]
13 });
lt()
, lte()
, gt()
, gte()
Performs integer comparison! Let’s see an example:1 await spec()
2 .get('<https://randomuser.me/api>')
3 .expectJsonMatch({
4 "results":
5 [
6 {
7 "dob":
8 {
9 "age": lt(100),
10 "children" : lte(2),
11 "salary" : gte(4000)
12 }
13 }
14 ]
15 });
notEquals()
Checks if the actual value is not equal to the expected one, and comes in handy when testing for wrong inputs! Example:
1 await spec()
2 .get('<https://randomuser.me/api>')
3 .expectJsonMatch({
4 "results": [
5 {
6 "name": notEquals('jon'),
7 }
8 ]
9 });
There are many more matching functions to make your life easier, which you can find at PactumJS API Documentation - Matching.
In this tutorial, we learned:
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.
DevOps Engineer, graduated from Computer Science, Ain Shams University. Linux Enthusiast.