Skip to main content

Jest Fundamentals

Jest is the most popular JavaScript testing framework. It comes with a test runner, assertion library, and mocking utilities — all in one package. Understanding its core API is essential before diving into TDD.

Setting Up Jest

Install Jest with TypeScript support:

npm install -D jest @types/jest ts-jest
npx ts-jest config:init

This creates a jest.config.js file. Add a test script to package.json:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  }
}

Jest automatically finds files matching *.test.ts, *.test.tsx, or *.spec.ts.

test() and describe()

The test() function defines a single test case. The describe() function groups related tests:

describe('MathUtils', () => {
  describe('add', () => {
    test('adds two positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('adds negative numbers', () => {
      expect(add(-1, -2)).toBe(-3);
    });

    test('adds zero', () => {
      expect(add(5, 0)).toBe(5);
    });
  });
});

Groups can nest, creating a hierarchy that reads like a specification. The test output mirrors this structure, making it easy to locate failures.

Essential Matchers

Jest's expect() function returns an object with matchers — methods that test different conditions.

Equality

// Strict equality (===) — use for primitives
expect(2 + 2).toBe(4);
expect('hello').toBe('hello');

// Deep equality — use for objects and arrays
expect({ name: 'Alice', age: 30 }).toEqual({ name: 'Alice', age: 30 });
expect([1, 2, 3]).toEqual([1, 2, 3]);

// Partial match — object contains these properties
expect(user).toMatchObject({ name: 'Alice' });

Truthiness

expect(value).toBeTruthy();    // value is truthy
expect(value).toBeFalsy();     // value is falsy
expect(value).toBeNull();      // value === null
expect(value).toBeUndefined(); // value === undefined
expect(value).toBeDefined();   // value !== undefined

Numbers

expect(price).toBeGreaterThan(0);
expect(price).toBeLessThanOrEqual(100);
expect(0.1 + 0.2).toBeCloseTo(0.3); // handles floating point

Strings

expect(message).toContain('success');
expect(email).toMatch(/^[\w.-]+@[\w.-]+\.\w+$/);

Arrays and Iterables

expect(fruits).toContain('apple');
expect(results).toHaveLength(5);
expect(tags).toEqual(expect.arrayContaining(['js', 'react']));

Exceptions

expect(() => divide(10, 0)).toThrow();
expect(() => divide(10, 0)).toThrow('Division by zero');
expect(() => divide(10, 0)).toThrow(ArithmeticError);

Negation

Any matcher can be negated with .not:

expect(value).not.toBe(0);
expect(array).not.toContain('banana');

Setup and Teardown Hooks

When multiple tests share setup logic, use hooks to avoid duplication:

describe('UserService', () => {
  let db: Database;

  beforeAll(async () => {
    // Runs once before all tests in this describe block
    db = await Database.connect();
  });

  afterAll(async () => {
    // Runs once after all tests
    await db.disconnect();
  });

  beforeEach(() => {
    // Runs before each test
    db.clear();
  });

  afterEach(() => {
    // Runs after each test
    jest.restoreAllMocks();
  });

  test('creates a user', async () => {
    const user = await UserService.create(db, { name: 'Alice' });
    expect(user.id).toBeDefined();
  });
});

Use beforeEach to ensure each test starts with a clean state. Use beforeAll for expensive setup that can be shared (like database connections).

Parameterized Tests with test.each

When you want to test the same logic with different inputs, use test.each instead of writing repetitive tests:

describe('isValidEmail', () => {
  test.each([
    ['user@example.com', true],
    ['user@sub.example.com', true],
    ['invalid-email', false],
    ['@no-local.com', false],
    ['no-domain@', false],
    ['', false],
  ])('isValidEmail("%s") returns %s', (email, expected) => {
    expect(isValidEmail(email)).toBe(expected);
  });
});

This runs six tests from a single test.each call. The output shows each case individually, so you know exactly which input failed.

You can also use an object syntax for clarity:

test.each([
  { input: 'hello', expected: 'HELLO' },
  { input: 'world', expected: 'WORLD' },
  { input: '', expected: '' },
])('toUpperCase("$input") returns "$expected"', ({ input, expected }) => {
  expect(input.toUpperCase()).toBe(expected);
});

Key Takeaways

  • Use describe to group tests and test to define individual cases.
  • toBe for primitives, toEqual for objects and arrays.
  • Use beforeEach for test isolation and beforeAll for shared expensive setup.
  • test.each eliminates repetitive tests — define inputs and expected outputs in an array.
  • Run jest --watch during development for instant feedback on file changes.