Skip to content

Jest with Node.js

Jest works just as well for backend Node.js code as it does for frontend components. This lesson covers testing patterns specific to Node.js: modules, environment variables, and the node test environment.

Testing Plain Node.js Modules

Backend Node.js code is often easier to test than UI code, since there's no DOM or rendering involved — you're usually testing pure functions, classes, or modules that read input and return output (or interact with mockable dependencies like a database or file system).

Set testEnvironment: 'node' (the default since Jest 28) for these projects, since there's no need for jsdom's overhead when nothing touches a simulated browser.

// slugify.js
function slugify(title) {
  return title.toLowerCase().trim().replace(/\s+/g, '-');
}
module.exports = { slugify };

// slugify.test.js
const { slugify } = require('./slugify');

test('converts a title into a URL-friendly slug', () => {
  expect(slugify('Hello World Article')).toBe('hello-world-article');
});

A pure Node.js utility function with no external dependencies is the simplest thing to test in any backend project.

Testing Code That Reads Environment Variables

test('uses the configured API base URL', () => {
  process.env.API_BASE_URL = 'https://test.example.com';

  const config = require('./config'); // re-require after setting env var

  expect(config.apiBaseUrl).toBe('https://test.example.com');
});
  • Set process.env.VAR_NAME before requiring a module that reads it at load time.
  • Use jest.resetModules() if a module caches its config at import time and needs a fresh read.
  • Restore environment variables in afterEach() to avoid one test's env leaking into another.
  • Consider a dedicated .env.test file (with a library like dotenv) for consistent test configuration.

Jest + Node.js Cheat Sheet

Common backend testing setups and patterns.

Scenario Approach
Pure utility function Import and call directly, assert on the result
Reads process.env Set env vars before requiring the module
Touches the file system Mock fs (see Mocking Node Modules)
Class with dependencies Inject mocked dependencies via the constructor
Test environment testEnvironment: 'node' (default since Jest 28)

Testing Classes with Injected Dependencies

Backend classes often depend on other services (a logger, a repository, an email client). Designing classes to accept dependencies through their constructor (dependency injection) makes them dramatically easier to test, since you can pass in mock versions directly without needing jest.mock() at all.

class OrderService {
  constructor(repository, emailClient) {
    this.repository = repository;
    this.emailClient = emailClient;
  }

  async placeOrder(order) {
    await this.repository.save(order);
    await this.emailClient.send(order.email, 'Order confirmed');
  }
}

test('saves the order and sends a confirmation email', async () => {
  const repository = { save: jest.fn() };
  const emailClient = { send: jest.fn() };
  const service = new OrderService(repository, emailClient);

  await service.placeOrder({ email: 'ada@example.com' });

  expect(repository.save).toHaveBeenCalled();
  expect(emailClient.send).toHaveBeenCalledWith('ada@example.com', 'Order confirmed');
});

Resetting the Module Registry

jest.resetModules() clears Jest's internal module registry, forcing the next require() call to load a fresh copy of a module. This matters for modules that cache configuration or state at load time based on environment variables or singletons.

beforeEach(() => {
  jest.resetModules();
  process.env = { ...ORIGINAL_ENV };
});

Common Mistakes

  • Importing a config module before setting the environment variables it depends on.
  • Hardcoding dependencies inside a class instead of injecting them, making mocking harder than necessary.
  • Forgetting to restore process.env after a test that modifies it.
  • Using testEnvironment: 'jsdom' for a pure backend project, adding unnecessary overhead.

Key Takeaways

  • Backend Node.js code is often simpler to test than UI code, with no DOM involved.
  • Dependency injection (passing dependencies via constructor/arguments) makes mocking straightforward.
  • Set environment variables before requiring modules that read them at load time.
  • jest.resetModules() forces a fresh module load, useful for config caching scenarios.

Pro Tip

Favor dependency injection over jest.mock() for your own classes and modules whenever practical — passing a mock object directly into a constructor is often simpler to read and maintain than mocking an entire imported module.