Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 30

Setting Up Jest and Supertest for Express Testing

Recommended resources

Introduction

In the last lesson, you confirmed that your API implementation matches its documentation by manually testing each endpoint through the Swagger UI. While effective, this manual process is slow and not easily repeatable. In a professional environment, and especially under the time pressure of an exam, you need a faster, more reliable way to ensure your API works as expected every time you make a change.

This lesson introduces automated integration testing. You will set up a professional testing environment using Jest, a popular JavaScript testing framework, and Supertest, a library for testing HTTP servers. The goal is to write code that tests your code, automating the verification process you previously did by hand.

Crucially, you will learn how to mock your Mongoose database model. This allows your tests to run without a live database connection, making them faster, more reliable, and completely self-contained—a mandatory requirement for your certification exam.


1. Installing Your Testing Toolkit

First, you need to add Jest and Supertest to your project. These are development dependencies, meaning they are only needed for testing and won't be part of your final production application.

Open your terminal in the project's root directory and run the following command:

npm install --save-dev jest supertest

Next, you need to tell npm how to run your tests. Open your package.json file and add a "test" script:

{
  "name": "your-project-name",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "test": "jest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  //... other fields
}

Now, when you run npm test, Jest will automatically search for and execute any files in your project ending with .test.js or located in a __tests__ directory.

Supertest express tutorial | Node REST API testing

The video 'Supertest express tutorial' by Jan Goebel provides a quick walkthrough of this initial setup.

Watch from 00:00:50 to 00:01:47 to see the installation of supertest and jest. Then, watch from 00:03:48 to 00:04:08 to see how the test script is added to package.json.


2. Making Your Express App Testable

There's a small but critical change you need to make to your project structure. Currently, your main server file (likely index.js or server.js) probably both defines the Express app and starts the server with app.listen().

Supertest needs to import the app object directly so it can make requests to it without actually starting a live server on a port. To enable this, we'll separate the app's definition from the server startup.

  1. If you have all your Express setup in server.js or index.js, create a new file named app.js in the root of your project.
  2. Move all of your Express application logic into app.js. This includes creating the app, setting up middleware (express.json(), cors), defining routes, and setting up error handlers.
  3. At the very end of app.js, export the app object. Do not call app.listen() in this file.

Your app.js should look something like this:

// app.js
const express = require('express');
const productRoutes = require('./src/routes/productRoutes'); // Adjust path as needed
const { notFound, errorHandler } = require('./src/middleware/errorMiddleware'); // Adjust path

const app = express();

app.use(express.json());

// --- Your other middleware ---

app.use('/api/products', productRoutes);

// --- Swagger setup ---

app.use(notFound);
app.use(errorHandler);

module.exports = app; // Export the app
  1. Now, modify your original server.js (or index.js) to be much simpler. It will now import the app from app.js and be responsible only for starting the server.

Your server.js should look like this:

// server.js
const app = require('./app'); // Import the configured app
const dotenv = require('dotenv');
const connectDB = require('./src/config/db'); // Adjust path

dotenv.config();
connectDB();

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => {
  console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});

This separation is a standard pattern that makes your application more modular and testable.

Mastering API Testing with Supertest, Express.js, and Jest

The article 'Mastering API Testing with Supertest, Express.js, and Jest' clearly explains this separation of concerns.

Read the section 'Crafting The Demo Express App'. Notice how it creates app.js to define and export the app, and server.js to import the app and start the server. This is the exact pattern you've just implemented.


3. Mocking the Database with jest.mock()

Your API routes interact with your Mongoose model to perform database operations. For your integration tests, you must not make actual database calls. Instead, you'll "mock" the model. Mocking means creating a fake version of the model that you can control from within your test.

Jest provides a powerful function, jest.mock(), to achieve this. When you mock a module, Jest replaces all of its exports with dummy functions. You can then specify what these dummy functions should return for a given test.

Let's see why and how this works.

Mocking a Database in Node with Jest

The video 'Mocking a Database in Node with Jest' by Sam Meech-Ward explains the concept of mocking and how to implement it perfectly for our use case.

Watch the following parts: 00:00:00 - 00:02:33: Understand why we mock the database to test the interactions in isolation. 00:03:19 - 00:05:05: See how jest.fn() is used to create mock functions that track calls and return values. 00:10:04 - 00:12:35: This is the most critical part. Pay close attention to how mockResolvedValue() is used to simulate the asynchronous behavior of a database call (like a Mongoose query). This is exactly what you will do.


4. Setting Up Your First Test File

Now, let's put all the pieces together.

  1. Create a new test file alongside your routes file. For example, if your routes are in src/routes/productRoutes.js, create src/routes/productRoutes.test.js.
  2. Add the following setup code to the top of productRoutes.test.js.
// src/routes/productRoutes.test.js

const request = require('supertest');
const app = require('../../app'); // Import the app object
const Product = require('../models/productModel'); // Import the Mongoose model

// Tell Jest to mock the Product model
jest.mock('../models/productModel');

describe('Product API', () => {
    
    // Test suite for GET /api/products
    describe('GET /api/products', () => {

        it('should return all products', async () => {
            // Define the mock data that the model should return
            const mockProducts = [
                { _id: '1', name: 'Product 1', price: 100 },
                { _id: '2', name: 'Product 2', price: 200 },
            ];
            
            // Configure the mock for the 'find' method
            Product.find.mockResolvedValue(mockProducts);

            // Make a request to the endpoint using Supertest
            const res = await request(app).get('/api/products');

            // Assertions
            expect(res.statusCode).toEqual(200);
            expect(res.body).toEqual(mockProducts);
            expect(Product.find).toHaveBeenCalledTimes(1);
        });

    });

    // You will add more test suites for POST, GET by ID, PUT, DELETE here
});

Let's break down this code:

  • const request = require('supertest');: Imports the Supertest library.
  • const app = require('../../app');: Imports your now-testable Express application.
  • jest.mock('../models/productModel');: This is the key instruction. It tells Jest to replace the real Product model with a mock. This line must come before you require the model.
  • const Product = require('../models/productModel');: This imports the mocked version of the model, not the real one.
  • Product.find.mockResolvedValue(mockProducts);: We are telling the mock find function to behave as if it's an async function that resolves with our mockProducts array.
  • await request(app).get('/api/products');: This is Supertest in action. It sends a GET request to your app's /api/products endpoint.
  • expect(...): These are Jest assertions. We check that the status code is 200, the body matches our mock data, and that the Product.find method was called exactly once.
Jest and Supertest Integration Test Examples
This image shows a typical integration test structure. A request is made to an endpoint, and then assertions are run against the response's status code and body, similar to the test you just wrote.

Now, you can run npm test in your terminal. You should see Jest find and pass your first test!


Conclusion

Congratulations, you have successfully configured a professional testing environment for your Express application. This setup is the foundation for ensuring your API is robust, reliable, and ready for your exam.

Key Takeaways:

  • Jest is your test runner and assertion library.
  • Supertest allows you to make HTTP requests to your Express app without needing a live server.
  • Separating your app definition from your server startup (app.listen) is essential for testability.
  • jest.mock() is the critical tool for isolating your routes from the database. It replaces your Mongoose model with a controllable fake.
  • Using mockResolvedValue() allows you to simulate the successful asynchronous responses of Mongoose methods like find(), findById(), and create().

Next Up:

With the testing framework in place, you are now ready to build out a complete test suite. In the next lesson, you will write integration tests for all of your CRUD endpoints, covering both successful operations and critical error cases like validation failures and "not found" scenarios.