Jest is a JavaScript testing framework that makes it incredibly easy to test Node.js backend services.

Let’s see it in action. Imagine a simple Express.js application with a single endpoint:

// src/app.js
const express = require('express');
const app = express();
const port = 3000;

app.use(express.json());

app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  // In a real app, you'd fetch this from a database
  if (userId === '123') {
    res.json({ id: '123', name: 'Alice' });
  } else {
    res.status(404).json({ message: 'User not found' });
  }
});

app.post('/users', (req, res) => {
  const newUser = req.body;
  // In a real app, you'd save this to a database
  res.status(201).json(newUser);
});

const server = app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

module.exports = server; // Export the server for testing

To test this, we’ll use Jest and a library called supertest to make HTTP requests to our Express app without actually spinning up a separate server process.

First, install the necessary development dependencies:

npm install --save-dev jest supertest

Now, let’s write our first test file, src/app.test.js:

// src/app.test.js
const request = require('supertest');
const server = require('./app'); // Import the server instance

describe('User API', () => {
  let appServer;

  // Start the server before all tests and close it after all tests
  beforeAll((done) => {
    appServer = server.listen(3001, done); // Use a different port for testing
  });

  afterAll((done) => {
    appServer.close(done);
  });

  it('should get a user by ID', async () => {
    const response = await request(appServer)
      .get('/users/123');

    expect(response.statusCode).toBe(200);
    expect(response.body).toHaveProperty('id', '123');
    expect(response.body).toHaveProperty('name', 'Alice');
  });

  it('should return 404 for non-existent user', async () => {
    const response = await request(appServer)
      .get('/users/999');

    expect(response.statusCode).toBe(404);
    expect(response.body).toHaveProperty('message', 'User not found');
  });

  it('should create a new user', async () => {
    const newUser = { name: 'Bob', email: 'bob@example.com' };
    const response = await request(appServer)
      .post('/users')
      .send(newUser);

    expect(response.statusCode).toBe(201);
    expect(response.body).toHaveProperty('name', 'Bob');
    expect(response.body).toHaveProperty('email', 'bob@example.com');
  });
});

To run these tests, add a script to your package.json:

{
  "scripts": {
    "test": "jest"
  }
}

Then, run npm test in your terminal.

The power here is that supertest acts like a real browser or client making requests, but it’s directly interacting with your Node.js application instance. This allows you to test your API endpoints, request/response handling, and even middleware without the overhead of network latency or setting up a separate HTTP server. You’re testing the logic of your application in isolation.

Jest’s describe blocks group related tests, and it blocks define individual test cases. beforeAll and afterAll are hooks that run once before and after all tests in a describe block, respectively. This is crucial for managing the lifecycle of your application server during testing. request(appServer) creates a supertest agent that targets your specific Express application instance.

The most surprising thing is how little code is actually needed to achieve robust API testing. You don’t need to mock network requests or worry about port conflicts; supertest handles the interaction seamlessly by passing requests directly to your application’s event handler.

The expect function from Jest is where the assertions happen. You can check status codes, response bodies, and even specific properties within the JSON response. The toHaveProperty matcher is particularly useful for verifying the structure and content of your API responses.

The core idea is to treat your backend service as a black box and test its inputs (HTTP requests) and outputs (HTTP responses). This promotes a clear separation of concerns and makes your code more maintainable. You can test success cases, error cases, edge cases, and different HTTP methods (GET, POST, PUT, DELETE, etc.) all within this framework.

If you were to expand this, you’d likely want to introduce database mocking or use an in-memory database for your tests to ensure true isolation and speed. This would prevent tests from interfering with each other and from relying on external state.

The next step is to explore testing asynchronous operations, like database calls, using Jest’s built-in support for Promises and async/await.

Want structured learning?

Take the full Jest course →