Mastering Jest for Express.js API Testing

Mastering Jest for Express.js API Testing

Testing is a crucial part of software development. It helps ensure that your code works as expected, catches bugs early in the development process, and provides confidence in the stability of your application. For Node.js and Express.js developers, Jest is a popular testing framework that makes testing easy and effective.

In this article, I'll dive into using Jest to test an Express.js API. I'll cover key concepts and walk through practical examples to help you master Jest for your projects.

Setting up the Environment

Before diving into Jest, let's set up our environment. We assume you already have an Express.js application in place. If not, you can quickly scaffold one with express-generator or set up a minimal Express app manually.

You can see how to set up an Express.js application with express-generator here

I'll use the below project structure as an example.

my-express-app/
  ├── src/
  │    └── middleware/
  │    │    └── validateSignupRequestBody.js
  │    └── app.js
  ├── __tests__/
  │    └── validateSignupRequestBody.test.js
  ├── jest.setup.js
  ├── package.json
  └── ...

We have an Express app with a middleware (validateSignupRequestBody.js) that validates the request body of a signup endpoint. Our goal is to test this middleware using Jest.

We will be testing the code in validateSignupRequestBody.js

const Joi = require('joi');

const requestBodySchema = Joi.object({
    name: Joi.string().min(4).required(),
    email: Joi.string().email().custom((value, helpers) => {
        if (!value.endsWith('@gmail.com')) {
          return helpers.error('any.invalid');
        }
        return value;
      }, 'Email Validation').required(),
      password: Joi.string().min(5).required(),
});

const validateSignupRequestBody = (req, res, next) => {
    try {
        const {error} = requestBodySchema.validate(req.body);

        if(error) {
            res.status(400).json({ message: "Invalid Request!", error: error.details[0].message });
            return;
        }

        next();
    } catch (error) {
        next(error);
    }
}

module.exports = validateSignupRequestBody;

Installing Jest

To get started, let's install Jest as a development dependency:

npm install --save-dev jest supertest @types/jest ts-jest @types/supertest

We've also added supertest and TypeScript-related dependencies because our example assumes you are working with TypeScript. If you are using plain JavaScript, you can skip the TypeScript dependencies.

Configuring Jest

module.exports = {
  testEnvironment: 'node',
  transform: {
    '^.+\\.ts$': 'ts-jest',
  },
  setupFilesAfterEnv: ['./jest.setup.js'],
};

This configuration tells Jest to use the ts-jest transformer for TypeScript files and to run the jest.setup.js script before each test suite. You can replace .ts with .js if you are using JavaScript.

Setting up a MongoDB Test Database

Many Express applications use databases like MongoDB. When testing, it's essential to isolate your tests from the production database. We'll use mongodb-memory-server to create an in-memory MongoDB instance for testing.

In the jest.setup.js file, add the following code:

const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');

let mongoServer;

beforeAll(async () => {
  mongoServer = await MongoMemoryServer.create();
  const uri = mongoServer.getUri();
  process.env.DATABASE_URL = uri;

  await mongoose.connect(uri, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });
});

afterAll(async () => {
  await mongoose.disconnect();
  await mongoServer.stop();
});

global.setupMongoDB = () => {
  // No need to redefine beforeAll and afterAll hooks here.
};

This code sets up an in-memory MongoDB server before your tests and shuts it down after testing is complete. It also exports a setupMongoDB function that you can use in your test files.

Writing Tests

Now that we have our environment set up, let's write some tests using Jest. We'll focus on testing the validateSignupRequestBody middleware. Create a test file named validateSignupRequestBody.test.ts (or .js for JavaScript) in the __tests__ directory with the following content:

const request = require('supertest');
const express = require('express');
const validateSignupRequestBody = require('path_to_the_validateSignupRequestBody.js')

const app = express();

// Middleware for parsing JSON request bodies
app.use(express.json());

// Register your middleware
app.use('/signup', validateSignupRequestBody);

describe('validateSignupRequestBody middleware', () => {
    // Use the global MongoDB setup function
    global.setupMongoDB();

    it('should return 400 Bad Request if the request body is missing required fields', async () => {
        const response = await request(app)
            .post('/signup')
            .send({});

        expect(response.status).toBe(400);
        expect(response.body).toMatchObject({
            message: 'Invalid Request!',
            error: expect.stringContaining('"name" is required'),
        });
    });

    it('should return 400 Bad Request if the email format is invalid', async () => {
        const response = await request(app)
            .post('/signup')
            .send({
                name: 'John',
                email: 'invalidemail',
                password: 'password123',
            });

        expect(response.status).toBe(400);
        expect(response.body).toMatchObject({
            message: 'Invalid Request!',
            error: expect.stringContaining('"email" must be a valid email'),
        });
    });

    it('should return 400 Bad Request if the email domain is not @gmail.com', async () => {
        const response = await request(app)
            .post('/signup')
            .send({
                name: 'John',
                email: 'example@yahoo.com',
                password: 'password123',
            });

        expect(response.status).toBe(400);
        expect(response.body).toMatchObject({
            message: 'Invalid Request!',
            error: expect.stringContaining('"email" contains an invalid value'),
        });
    });
});

In this test file:

  • We create an Express application and apply the express.json() middleware for parsing JSON request bodies.

  • We register the validateSignupRequestBody middleware on the /signup route.

  • We use Jest's describe and it functions to structure our tests.

  • We use supertest to make HTTP requests to our Express app and assert the responses.

  • We utilize the global.setupMongoDB() function to set up and tear down an in-memory MongoDB server for our tests.

Running Tests

With everything set up and tests written, you can now run your tests using Jest:

npm test

Jest will execute your tests, and you should see the test results in your terminal.

Conclusion

Testing your Express.js API with Jest is a powerful way to ensure your application behaves as expected. In this article, we've covered the essential steps to set up Jest for testing Express.js applications, including configuring Jest, setting up an in-memory MongoDB test database, and writing test cases.

With Jest, you can confidently test your middleware, route handlers, and other parts of your Express.js application, helping you catch and fix issues early in the development process. Whether you're building a small API or a large-scale application, mastering Jest for testing is a valuable skill that can lead to more reliable and maintainable code.