Jest is a JavaScript-based testing framework that lets you test both front-end and back-end applications. Jest is great for validation because it comes bundled with tools that make writing tests more manageable. While Jest is most often used for simple API testing scenarios and assertions, it can also be used for testing more complex data structures. In this tutorial, I will lead you through how to use Jest to test nested API responses and hopefully have fun in the process.
Prerequisites
To complete this tutorial, you will need:
- Node.js installed on your system.
- A CircleCI account.
- A GitHub account.
- Postman or any other HTTP client to test the API.
- Basic knowledge of JavaScript.
- Basic understanding of APIs and unit testing. .
Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.
Cloning the repository
This tutorial will take you through the process of writing Jest tests for a space API demonstration project. To access the entire project, clone the repository by running this command:
git clone --single-branch --branch base-project https://github.com/CIRCLECI-GWP/space-api-project
Then run:
cd space-api-project
Creating your Space API
To start writing cool Jest tests, you first need to have your API running. This is what you will write tests against. For this tutorial, you will use a simple API that will give you responses you can use to write your tests. I have chosen an API based on space travel, so gear up, prospective space explorer!
The API for this tutorial has already been developed. Set it up with this command to install the application dependencies:
npm install
Start the application (API server):
npm start
After running these commands, you can start writing tests.
The API returns responses of the most crucial items required for space travel. It has these endpoints:
- Available space destinations
- Available flights and related information
- A selection of flight seats and their configurations
Note: The focus of this tutorial is testing with Jest, not developing a space API repository, so there are no steps for that. If you are curious about all the endpoints available, or about the application architecture, go to the routing file of the application here.
Assuming you have cloned and set up the application on your machine, all endpoints are already present. Make a GET
request with an API client like Postman to http://localhost:3000/space/flights
, http://localhost:3000/space/destinations
and http://localhost:3000/space/flights/seats
.
The details have been hard coded onto the API endpoints.
Now that you have experienced how your API works and simulates the responses, you can move on to testing.
Writing API tests with Jest
In the previous section, you set up an API to receive responses from the different API routes. In this section, you will test your API responses, understand how Jest can be used to test nested JavaScript objects and arrays, and learn how this will improve your testing skills as a developer or a test engineer. You will use JEST to test the different combinations of your API responses, ranging from a simple array to a complicated response of arrays containing objects and even an object containing arrays with more child arrays and objects.
Installing Jest
To begin testing with Jest, you need to add it to your currently existing space API. To install Jest as a development dependency, run this command.
npm install --save-dev jest
Note: Development dependencies are dependencies that are required for development purposes only. These dependencies should not be installed in a production environment.
After successfully running the installation, you need to make some changes to your package.json
file to make sure that your space tests are run with the proper configuration. You will add two commands, one command for testing your API endpoints, and another command to request that Jest watch for changes in your files and re-run the tests. Add the Jest command on the package.json file:
"scripts": {
"start": "node ./bin/www",
"test": "jest"
},
You could add a Jest configuration file instead, if that works better for your project. For this tutorial, though, you will use Jest’s default configuration.
Setting up API testing in Jest
For your first test, create a tests
folder in the root directory. Inside this folder, create a test file the main routes and add the .spec.js
extension to it. In your case, that will be space.spec.js
. Adding the .spec.js
extension tells Jest that the file is a test.
Like space shuttle launches, you begin your test process with static fires to check that everything in your tests configuration is working correctly.
Inside the tests/space.spec.js
file, implement this test using this code:
describe("Space test suite", () => {
it("My Space Test", () => {
expect(true).toEqual(true);
});
});
Now when you run npm test
on your terminal, you will get a successfully executing test. When the test runs and passes, the test results on the terminal should be green. You can further change the assertion from true
to false
and the execution on your terminal will result in red, signifying a failure.
Before you start the rocket launch sequence, you should add Jest’s watch
command in your package.json
. Adding this command means that when your application code changes the tests are re-run. This is possible when you have your application running in one terminal while your tests are in a different terminal. Add this command in the package.json
file:
"scripts": {
"start": "node ./bin/www",
"test": "jest",
"test:watch": "npm run test -- --watch"
},
When you execute your Node.js API while running the watch command, you can observe the re-run of your tests in real time when you change any code. This can come in handy to detect when your API breaks due to a change. Run the application in one terminal window, side-by-side with the tests, showing the watch option (on the right).
Testing your Space API
To make API requests in your Jest tests, you require a module that will query your endpoints and return the responses to your tests. That module is SuperTest, and you can install it using this command:
npm install --save-dev supertest
Jest and SuperTest are set up, and a basic test has been written in the Jest testing framework. In this section, you will focus on testing the different API response objects provided by your endpoints. Start with the response provided by the endpoint http://localhost:3000/space/destinations
. It is a simple array of objects representing the destinations space flights will travel to.
[
"Mars",
"Moon",
"Earth",
"Mercury",
"Venus",
"Jupiter"
]
An array is one of the most basic responses that you can receive from an API endpoint, yet Jest provides you with countless ways of asserting that the responses received meet your expectations.
Your first actual test will focus on this array of objects. Replace the contents of the space.spec.js
file with this code snippet.
const request = require("supertest");
const app = require("../app");
describe("Space test suite", () => {
it("tests /destinations endpoints", async () => {
const response = await request(app).get("/space/destinations");
expect(response.body).toEqual(["Mars", "Moon", "Earth", "Mercury", "Venus", "Jupiter"]);
expect(response.body).toHaveLength(6);
expect(response.statusCode).toBe(200);
// Testing a single element in the array
expect(response.body).toEqual(expect.arrayContaining(["Earth"]));
});
// Insert other tests below this line
// Insert other tests above this line
});
In this test, you get to interact with the API using SuperTest. The test first fetches the response from the /space/destinations
endpoint, then uses the response to test it against a couple of assertions. The most notable assertion is the last one that uses the Jest array arrayContaining()
method to verify that a single item can be found in an array. In this assertion, Jest maps through the array and knows which array elements are present in the array and which ones are not.
While the array above contained only five items, it is likely you will encounter more complicated scenarios where you will not be able to fully test the whole object without breaking it down.
In the next test, you will test the /space/flights/seats
endpoint. This endpoint provides a response that is more complicated than the array one above, and you will again use Jest to test it.
Given the nature of your response, you will use a section of the received response from the endpoint and this will give you a template of tests that you expect to write for subsequent objects in the response.
{ "starship": [
{
"firstClass": {
"heatedSeats": true,
"chairOptions": [
"leather",
"wollen"
],
"vaultAccess": true,
"drinksServed": [
"tea",
"coffee",
"space-special",
"wine"
],
"windowAccess": true,
"privateCabin": "2",
"VRAccess": "unlimited",
"cost": "$20000",
"seatHover": {
"cryoMode": [
"extreme",
"ludacris",
"plaid"
],
"staticMode": [
"ludacris",
"plaid"
]
}
},
"businessClass": { ... }
}
],
"blueOrigin": [
{
"firstClass": { ... },
"businessClass": { ... }
}
]
}
Note: You can always view the full response by calling the endpoint using a tool like Postman.
The focus for this test is writing an exhaustive test for the object while at the same time ensuring that the test is readable and simple. Given the complicated nature of the response, you will try to cover as much as possible while keeping the test readable.
...
it('tests /space/flights/seats endpoint - starship', async () => {
const response = await request(app).get("/space/flights/seats");
expect(response.body.starship).toEqual(expect.arrayContaining([expect.any(Object)]));
// Checking that the starship Object contains firstClass Object which then contains a set of objects
expect(response.body.starship).toEqual(expect.arrayContaining(
[expect.objectContaining({ firstClass: expect.any(Object) })]));
expect(response.body.starship).toEqual(expect.arrayContaining([expect.objectContaining(
{ businessClass: expect.any(Object) })]));
// Checking that under the bussinessClass Object we have the array drinks served
expect(response.body.starship)
.toEqual(expect.arrayContaining([expect.objectContaining({
businessClass: expect.objectContaining({ drinksServed: expect.any(Array) })
})]));
// Checking that under the firstClass: Object we have the option ludacris in the seatHover Object
expect(response.body.starship)
.toEqual(expect.arrayContaining([expect.objectContaining({
firstClass: expect.objectContaining({ seatHover: expect.objectContaining({
cryoMode : expect.arrayContaining(['ludacris'])}) })
})]));
// Checking that under the firstClass: Object we have the option plaid in the seatHover Object
expect(response.body.starship)
.toEqual(expect.arrayContaining([expect.objectContaining({
firstClass: expect.objectContaining({ seatHover: expect.objectContaining({
staticMode : expect.arrayContaining(['plaid'])}) })
})]));
});
...
This test traverses the different levels of the provided object above using Jest matchers. It first tests the top-level objects, narrowing down to the objects and the arrays that have been nested deeply into the response. The test might look intimidating at first, but it shows that you can specifically test areas of the responses of an API without asserting the whole response object.
In the next section, you will learn how you can write custom error messages in Jest before your launch into space!
Using Jest custom matchers for error messaging
Jest matchers let you test data in different ways. For example, you can explicitly test a specific condition and even extend your own custom matcher if that condition is not covered by an existing one. Jest provides a way to declare custom matchers and have your own custom errors when running tests. Once you are in space, it would be great to have some level of control by overriding commands given by the command center. You do not want to crash into a meteorite or space debris if there is something you can do something to prevent it. To do this, you will use the response from the /space/flights
API endpoint that you created above.
Jest provides the expect.extend(matchers)
method so you can add custom expectations. In this tutorial, you want to validate that you can book only those flights that are active
. In the flights endpoint you created, the object returns the different flights with their active status either set to true
or false
. Instead you can try writing a matcher for this.
To create a test that uses a custom matcher, you need to declare the matcher and define what parameters would lead to passing or failing. This matcher can be declared anywhere inside the Jest describe
block. For readability, place it on top of the test file, immediately after the describe block. For this project, you want to check that the returned flights have a status of active.
...
expect.extend({
toBeActive(received) {
const pass = received === true;
if (pass) {
return {
message: () =>
`expected ${received} is an acceptable flight status`,
pass: true,
};
}
}
});
...
The custom matcher expects to receive the value of the state of the flight and return a custom message. There is a slight problem, though. When you run your tests, you get the wrong message even when the test does not meet the expectations of the passed flight status value. In other words, a false negative. To fix this, add an else
statement to accommodate scenarios where your assertions will fail the criteria. The else statement will capture all the false negatives that could happen when running the test, and report them. Update the code like this:
...
expect.extend({
toBeActive(received) {
const pass = received === true;
if (pass) {
return {
message: () =>
`expected ${received} to be an acceptable flight status`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be an acceptable flight status of flight - only true is acceptable`,
pass: false,
};
}
},
});
...
The first custom message is launched when the test has a false negative. The second is launched when the test has failed by receiving a false
value from the active state of the space flight.
Add this code block below the expect.extend
code block you added previously.
...
it('tests /space/flights endpoint - positive test', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[0].active).toBeActive();
});
it('tests /space/flights endpoint - false negative', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[0].active).not.toBeActive();
});
it('tests /space/flights endpoint - failing test', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[1].active).toBeActive();
});
...
When you run these tests, some fail with this output:
> jest
FAIL tests/space.spec.js
Space test suite
✓ tests /destinations endpoints (25 ms)
✓ tests /space/flights/seats endpoint - starship (4 ms)
✓ tests /space/flights endpoint - positive test (3 ms)
✕ tests /space/flights endpoint - false negative (2 ms)
✕ tests /space/flights endpoint - failing test (3 ms)
...
GET /space/destinations 200 2.851 ms - 51
GET /space/flights/seats 200 0.699 ms - 1073
GET /space/flights 200 0.610 ms - 1152
GET /space/flights 200 0.382 ms - 1152
GET /space/flights 200 0.554 ms - 1152
Test Suites: 1 failed, 1 total
Tests: 2 failed, 3 passed, 5 total
Snapshots: 0 total
Time: 0.512 s, estimated 1 s
Ran all test suites.
Note: The two failing tests have been written, as is, for demonstration purposes. To make the tests pass on the CI, skip them with the it.skip()
function so they are not run by default.
...
it('tests /space/flights endpoint - positive test', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[0].active).toBeActive();
});
it.skip('tests /space/flights endpoint - false negative', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[0].active).not.toBeActive();
});
it.skip('tests /space/flights endpoint - failing test', async () => {
const response = await request(app).get("/space/flights");
expect(response.body[1].active).toBeActive();
});
...
And you have passing tests. Countercheck the final state of the tests/space.spec.js
file on GitHub.
Adding CircleCI configuration file
Here, you will add the pipeline configuration for CircleCI to your local project. Start by creating a .circleci
folder in the root directory. Inside that folder, create a config.yml
file. Now add details to the config.yml
file by configuring it as shown in this code block:
version: 2.1
jobs:
build:
working_directory: ~/repo
docker:
- image: cimg/node:21.4.0
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: install dependencies
command: npm install
- save_cache:
key: dependency-cache-{{ checksum "package-lock.json" }}
paths:
- ./node_modules
- run:
name: run space test
command: npm test
- store_artifacts:
path: ~/repo/space
In this configuration, CircleCI uses a node Docker image, pulled from the environment, then updates the npm package manager. The next stage is to restore the cache if it exists. The application dependencies are updated only when a change has been detected with save-cache
. Finally, you will run your space tests and store the cached items in the artifacts’ space
directory.
Now, if you cloned the sample project repository, it is already initialized and set up in git. It can be helpful to understand how your project is integrated with CircleCI. To set up CircleCI, initialize a GitHub repository in your project by running the command:
git init
Add a commit and then push your project to GitHub.
Next, log in to your CircleCI account. If you signed up with your GitHub account, all the repositories associated with your GitHub username will be available on your project’s dashboard. Select the space-api-project
project from the list.
Click the Set Up Project button. You will be prompted about whether you have already defined the configuration file for CircleCI within your project. Enter the branch name (for the tutorial, you are using main). Click Set Up Project button to complete the process.
This will start the CircleCI pipeline and automatically run the tests successfully!
Conclusion
In this tutorial, you have set up a space-api, installed Jest, and written tests against the API. You also learned how to use Jest matchers and even write your own custom matchers. Through the tutorial, you also covered how to integrate CircleCI into a project, push tests to GitHub, and create different build steps for the CI pipeline. Finally, you covered running your tests on the pipeline and verifying that all the steps of the pipeline are working as expected.
All systems are go for your launch into space. Happy T-minus-zero!