← Back to DevBytes

Building Full-Stack Apps with Jest

Introduction to Full-Stack Testing with Jest

Jest is a delightful JavaScript testing framework developed by Facebook, designed to work out of the box with minimal configuration. While it is famously used for testing React applications, Jest is equally powerful for testing the backend (Node.js, Express) and even full-stack integrations. "Building Full-Stack Apps with Jest" means writing unit, integration, and snapshot tests that cover both your frontend components and your backend APIs, ensuring the entire application behaves correctly from database to UI.

What Jest Offers for Full-Stack Development

Jest provides a unified test runner, assertion library, mocking utilities, and code coverage reporting. Its key features include:

Why Jest Matters for Full-Stack Development

When building a full-stack application, the risk of integration bugs increases. Jest helps you catch issues early by allowing you to test each layer in isolation and then together. It ensures that your frontend components render correctly with mock data, your backend endpoints return expected responses, and that changes to one part of the stack don’t break another. Moreover, Jest’s fast feedback loop and rich error messages make it the go‑to choice for modern JavaScript teams.

Setting Up Jest in a Full-Stack Project

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Installing Jest and Dependencies

For a typical full-stack project using Node.js, Express on the backend, and React on the frontend, start by installing Jest along with the necessary testing libraries:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event supertest

Add a test script to your package.json:

"scripts": {
  "test": "jest --watchAll",
  "test:coverage": "jest --coverage"
}

If you use a monorepo (e.g., with Lerna or npm workspaces), you can install Jest at the root and configure it to run across packages. A basic jest.config.js might look like:

module.exports = {
  testEnvironment: 'node',   // for backend tests; override in frontend
  roots: ['/backend', '/frontend'],
  transform: {
    '^.+\\.jsx?$': 'babel-jest'
  }
};

For frontend tests, you may want a separate Jest config or use @testing-library/jest-dom setup by adding a setup file:

// jest.setup.js
import '@testing-library/jest-dom';

And reference it in your Jest config:

setupFilesAfterEnv: ['./jest.setup.js']

Testing the Backend API with Jest

Unit Testing Express Routes

To test Express routes, use supertest to make HTTP requests to your app without starting a server on a real port. Create a test file, e.g., routes.test.js:

const request = require('supertest');
const app = require('../app'); // your Express app

describe('GET /api/users', () => {
  it('should return a list of users', async () => {
    const res = await request(app).get('/api/users');
    expect(res.statusCode).toEqual(200);
    expect(res.body).toHaveProperty('users');
    expect(Array.isArray(res.body.users)).toBe(true);
  });

  it('should return 404 for unknown route', async () => {
    const res = await request(app).get('/api/unknown');
    expect(res.statusCode).toEqual(404);
  });
});

Make sure your Express app is exported without calling app.listen() inside the same file. A common pattern:

// app.js
const express = require('express');
const app = express();
// ... routes
module.exports = app;

Mocking Database Calls

To avoid hitting a real database, mock your database layer (e.g., Mongoose, Prisma, or raw SQL). Use Jest’s jest.mock() to replace the module:

// __mocks__/userModel.js
module.exports = {
  findAll: jest.fn(() => Promise.resolve([{ id: 1, name: 'Alice' }])),
  findById: jest.fn((id) => Promise.resolve({ id, name: 'Bob' })),
};

// In your test file
jest.mock('../models/userModel');
const userModel = require('../models/userModel');

describe('GET /api/users/:id', () => {
  beforeEach(() => {
    userModel.findById.mockClear();
  });

  it('should return a single user', async () => {
    const res = await request(app).get('/api/users/1');
    expect(res.statusCode).toBe(200);
    expect(res.body.name).toBe('Bob');
    expect(userModel.findById).toHaveBeenCalledWith('1');
  });

  it('should handle errors gracefully', async () => {
    userModel.findById.mockRejectedValue(new Error('DB error'));
    const res = await request(app).get('/api/users/99');
    expect(res.statusCode).toBe(500);
    expect(res.body.error).toBe('Internal server error');
  });
});

Testing the Frontend with Jest

Component Testing with React Testing Library

Jest pairs perfectly with React Testing Library to test component behavior. Example: testing a UserList component that fetches and displays users.

// UserList.jsx
import React, { useState, useEffect } from 'react';

export default function UserList() {
  const [users, setUsers] = useState([]);
  const [error, setError] = useState('');

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => setUsers(data.users))
      .catch(() => setError('Failed to load users'));
  }, []);

  if (error) return <div>{error}</div>;
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Test file (UserList.test.jsx):

import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';

beforeEach(() => {
  global.fetch = jest.fn();
});

afterEach(() => {
  delete global.fetch;
});

test('displays users when API call succeeds', async () => {
  const mockUsers = { users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] };
  global.fetch.mockResolvedValueOnce({
    json: async () => mockUsers,
  });

  render(<UserList />);
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('Bob')).toBeInTheDocument();
  });
});

test('displays error message on API failure', async () => {
  global.fetch.mockRejectedValueOnce(new Error('Network error'));

  render(<UserList />);
  await waitFor(() => {
    expect(screen.getByText('Failed to load users')).toBeInTheDocument();
  });
});

Mocking API Calls in Frontend Tests

Instead of mocking global.fetch manually, you can use libraries like msw (Mock Service Worker) for more robust API mocking. However, for simplicity, Jest’s manual mock is sufficient for most unit tests. Always clean up mocks after each test to avoid test contamination.

Integration and End-to-End Testing

Combining Backend and Frontend Tests

Integration tests validate that the backend and frontend work together. You can write tests that spin up a real server (or use supertest) and then use a headless browser like Puppeteer or Playwright. Jest supports custom test environments. For example, a simple integration test using supertest and a static frontend build:

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

describe('Full stack integration: /api/users and frontend', () => {
  it('returns user data that matches the UI expectations', async () => {
    const res = await request(app).get('/api/users');
    expect(res.statusCode).toBe(200);
    // Assume frontend expects an array under "users"
    expect(res.body).toHaveProperty('users');
    // Further validation can mimic frontend logic
    res.body.users.forEach(user => {
      expect(user).toHaveProperty('id');
      expect(user).toHaveProperty('name');
    });
  });
});

For true end-to-end tests, consider a separate test suite using Cypress or Playwright, but Jest can still be used to run them if you configure the proper test environment.

Best Practices for Full-Stack Jest Testing

Conclusion

Jest is a versatile and powerful framework that scales from a simple unit test for a utility function to a full suite of integration tests covering an entire full-stack application. By following the patterns shown in this tutorial—mocking dependencies, testing API endpoints with Supertest, verifying React components with Testing Library, and structuring tests for both layers—you can achieve high confidence in your codebase. Remember that the goal of full-stack testing is not 100% coverage but rather ensuring that each part of your stack works correctly in isolation and together. With Jest’s rich ecosystem and minimal configuration, you can build robust, maintainable tests that keep your full‑stack app reliable as it grows.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles