API Testing 101: A Beginner's Guide to
Testing Nodejs APIs with Jest and
Supertest

Photo by AltumCode on Unsplash

API Testing 101: A Beginner's Guide to Testing Nodejs APIs with Jest and Supertest

In today’s fast-paced software development world, testing is becoming an essential part of the software development life cycle, and with the increasing rise of web and mobile applications, bug-free, reliable, and efficient APIs are crucial to meeting various business needs.

This guide will provide a starting point for Node.js developers who want to understand the fundamentals of testing and writing API tests for backend applications using Jest and Supertest.

We will cover topics like software testing, types of testing, testing frameworks, and a step-by-step walk-through of a sample test code for a basic create, read, update, and delete (CRUD) operations API.

To follow the guide completely, you’re required to know the fundamentals of building an API with Node.js as well as any Node.js framework (e.g., Express.js, Nest.js, etc.). However, you can follow the first few sections before the code section with little-to-no knowledge of backend development.

Software testing

Firstly, let us know what software testing is before proceeding to understand API testing. According to a post on IBM, software testing is the process of evaluating and verifying that a software product or application does what it is supposed to do. It involves providing edge cases to test that the application works as expected.

What is API testing?

It’s simply a form of software testing that evaluates and verifies that an API functions as expected. It is done to test the API’s security, performance, and reliability. It generally involves simulating a request to the API endpoints to test that the responses match the expected results.

Importance of API testing

According to a survey conducted by SmartBear in 2020, 90% of organizations said they either currently have or plan to have a formal API testing process in place in the near future. This highlights the importance of API testing and the shift towards it.

A 2020 survey by smartBear illustrating the importance of API testing in business organizations.

The following are more benefits of API testing:

  • It ensures that the API returns correct responses every time.

  • It helps to identify security vulnerabilities to prevent the API from being compromised by attackers.

  • It allows for detailed and accurate documentation of the API.

  • It can help identify performance issues.

  • It helps to deliver a high-quality API with fewer bugs.

Types of Testing

There are many types of testing, but we will be looking at the two most common types of testing for backend developers.

Unit testing

Unit testing is a type of testing that focuses on testing individual components of a codebase. In API development, unit testing is used to test individual routes, middlewares, or functions without considering their interaction with other components of the system like databases, caching systems, internal services, or other external services.

Integration testing

Integration testing is a type of testing that tests logically grouped modules together. It also focuses on testing the interaction between different components or services of an application. In API development, integration testing is used to test the interaction between the API’s routes, databases, middlewares, or functions with each other as well as other internal or external services.

Testing frameworks and libraries

Testing frameworks and libraries are software applications or packages that are used to perform testing in software development. There are several testing tools and libraries for different purposes and programming languages. However, the common testing frameworks and libraries for Node.js applications and APIs are listed below:

  • Jest

  • Supertest

  • Mocha

  • Superagent

Let’s take a look at what Jest and Supertest do.

Jest: Jest is a testing framework or tool that is used to write tests for any JavaScript codebase, including Node.js projects. It’s easy to use, well-documented, has little-to-no configuration, and can be customized to meet one’s needs.

Supertest: Supertest is a testing library that is used to test Node.js APIs. It is best used along with a testing framework like Jest or Mocha.

In the next sections, we’ll be looking at how to use Jest and Supertest to test a Node.js (Express) API.

Express server setup

In this section, we’ll be developing a book API that allows for creating, retrieving, updating, and deleting books from a database.

NB: The code in this section was created only to demonstrate the basic concepts of testing an API and is, by no means, an example of how a modern web API or test should be made or structured.

Before we proceed, make sure you have Node.js installed on your machine.

Follow the steps below to set up an Express server:

  1. Firstly, create a new directory for the book API and open it in your preferred editor.

  2. Navigate to the directory in your terminal and run npm init -y to initialize a new Node.js project.

  3. In your terminal, run npm install express mongoose to install the express and mongoose packages.

  4. Now, create a new file named server.js and add the following code to it:

const express = require('express');
const mongoose = require('mongoose');

// Create a schema for your data
const bookSchema = new mongoose.Schema({
  title: String,
  author: String,
  genre: String
});

// Create a model based on the schema
const Book = mongoose.model('Book', bookSchema);

// Create an Express app
const app = express();
app.use(express.json());

app.get('/', (req, res) => {
  res.json({ text: 'Server started on port 3001' })
})

// Create a new book
app.post('/books', (req, res) => {
  const { title, author, genre } = req.body;
  const book = new Book({
    title,
    author,
    genre
  });

  book.save().then((result) => {
    res.json(result);
  }).catch((error) => {
    res.status(500).json({ error: 'Error creating book' });
  });
});

// Get all books
app.get('/books', (req, res) => {
  Book.find().then((books) => {
    res.json(books);
  }).catch((error) => {
    res.status(500).json({ error: 'Error retrieving books' });
  });
});

// Get a single book by ID
app.get('/books/:id', (req, res) => {
  const { id } = req.params;

  Book.findById(id).then((book) => {
    if (book) {
      res.json(book);
    } else {
      res.status(404).json({ error: 'Book not found' });
    }
  }).catch((error) => {
    res.status(500).json({ error: 'Error retrieving book' });
  });
});

// Update a book by ID
app.put('/books/:id', (req, res) => {
  const { id } = req.params;
  const { title, author, genre } = req.body;

  Book.findByIdAndUpdate(id, {
    title,
    author,
    genre
  }, { new: true }).then((book) => {
    if (book) {
      res.json(book);
    } else {
      res.status(404).json({ error: 'Book not found' });
    }
  }).catch((error) => {
    res.status(500).json({ error: 'Error updating book' });
  });
});

// Delete a book by ID
app.delete('/books/:id', (req, res) => {
  const { id } = req.params;

  Book.findByIdAndRemove(id).then((book) => {
    if (book) {
      res.json(book);
    } else {
      res.status(404).json({ error: 'Book not found' });
    }
  }).catch((error) => {
    res.status(500).json({ error: 'Error deleting book' });
  });
});

// Start the server
const server = app.listen(3001, async () => {
  console.log('Server started on port 3001');
  await mongoose.connect('mongodb+srv://technical-guide:uQxSkFkzgx29r3E8@test-cluster.ne5tsho.mongodb.net/book-api?retryWrites=true&w=majority', {
    useNewUrlParser: true,
    useUnifiedTopology: true
  });
});

module.exports = server;
  1. Save the file, return to your terminal, and run node server.js. You should have something like this:

We’ve successfully created a basic CRUD API with Express.js and MongoDB.

Since this guide is mainly focused on testing, I will briefly explain what the above API does without being too detailed.

The book API allows users to interact with a collection of books. It establishes a connection to a MongoDB database and defines a schema for the book data. Users can create a new book by sending a POST request with the book's details, retrieve all books with a GET request, fetch a specific book by its ID using a GET request with the corresponding book ID, update a book's information by sending a PUT request with the updated details and the book's ID, and delete a book by its ID using a DELETE request. And the server runs on port 3000.

Testing the book API with Jest and Supertest

To start testing the book API with Jest and Supertest, we would need to first download the two packages by navigating to the project’s directory and then running npm install --save-dev jest and npm install --save-dev supertest. The --save-dev flag is used to install the packages as development dependencies.

Next, create a file named server.test.js. This is where our test code will live. We’re saving the file with a file extension of test.js because Jest recognizes files with this extension as test files.

Now, copy the code below into the server.test.js file (the explanation comes briefly).

const request = require('supertest');
const app = require('./server');
const mongoose = require('mongoose');

describe('Book API Endpoints', () => {
  let bookId;

  afterAll((done) => {
    app.close(done);
    mongoose.disconnect();
  });

  it('should create a new book', async () => {
    const res = await request(app)
      .post('/books')
      .send({
        title: 'Test Book',
        author: 'Test Author',
        genre: 'Test Genre',
      });

    expect(res.statusCode).toEqual(200);
    expect(res.body.title).toEqual('Test Book');
    expect(res.body.author).toEqual('Test Author');
    expect(res.body.genre).toEqual('Test Genre');
    bookId = res.body._id;
  });

  it('should get all books', async () => {
    const res = await request(app).get('/books');
    expect(res.statusCode).toEqual(200);
    expect(Array.isArray(res.body)).toBeTruthy();
  });

  it('should get a single book by ID', async () => {
    const res = await request(app).get(`/books/${bookId}`);
    expect(res.statusCode).toEqual(200);
    expect(res.body.title).toEqual('Test Book');
    expect(res.body.author).toEqual('Test Author');
    expect(res.body.genre).toEqual('Test Genre');
  });

  it('should update a book', async () => {
    const res = await request(app)
      .put(`/books/${bookId}`)
      .send({
        title: 'Updated Book',
        author: 'Updated Author',
        genre: 'Updated Genre',
      });
    expect(res.statusCode).toEqual(200);
    expect(res.body.title).toEqual('Updated Book');
    expect(res.body.author).toEqual('Updated Author');
    expect(res.body.genre).toEqual('Updated Genre');
  });

  it('should delete a book', async () => {
    const res = await request(app).delete(`/books/${bookId}`);
    expect(res.statusCode).toEqual(200);
    expect(res.body.title).toEqual('Updated Book');
    expect(res.body.author).toEqual('Updated Author');
    expect(res.body.genre).toEqual('Updated Genre');
  });
});

Let's go through the test code.

We import the necessary modules, including request, a Supertest method that allows you to interact with your server as if it were running and receiving real HTTP requests. We’re also importing app from our server.js file and mongoose for database operations.

The Jest describe function is used to group related test cases. In this case, all the test cases are related to the Book API endpoints.

The afterAll hook provided by Jest is used to close the server and disconnect from the MongoDB database after all the test cases in the test suite are executed. This ensures that we clean up resources after testing; otherwise, Jest wouldn’t exit after the test completes.

Each test case is represented by an it block, which contains the test name and the test code.

The test cases use Supertest to make HTTP requests to the API endpoints and check the responses against the expected results using the expect function.

In the first test case, we create a new book by sending a POST request to the /books endpoint and checking if the response status code is 200 (OK) and if the response body contains the correct book information.

We save the bookId from the response for later use in other test cases.

In the second test case, we fetch the book that we created in the previous test case. We do this by sending a GET request to the endpoint /books/:id, where :id is the bookId of the book we created in the first test case.

In the third test case, we update the book's information. We do this by sending a PUT request to the /books/:id endpoint with the updated book data. Again, we use the bookId variable we saved in the first test case to target the specific book we created earlier.

In the fourth test case, we delete the book that we created in the first test case. We do this by sending a DELETE request to the /books/:id endpoint, using the bookId variable to target the specific book.

Now, we have everything set up to run the tests for our Book API. To do that, we need to add a test script to the package.json file:

"scripts": {
    "test": "jest --testTimeout 20000"
},

In the "scripts" section of the package.json file, we add a "test" script that runs Jest with a test timeout of 20,000 milliseconds (20 seconds). The test timeout is essential to allow enough time for the API tests to complete because they involve interactions with the database.

To run the tests, open your terminal and run the following command:

npm test

Jest will execute the test cases in the server.test.js file, and you should see the test results in the terminal.

Hurray. The test passed. Congratulations on writing your first test!

Conclusion

In conclusion, this beginner's guide has provided you with a solid starting point for understanding API testing and how to test Node.js APIs using Jest and Supertest. To further enhance your skills, I encourage you to explore additional resources available online, including the official documentation for Jest and Supertest. Also, Practising with various testing scenarios will help you gain confidence and expertise in API testing, making you a more proficient developer in today's software development landscape.