Jest is the most popular test framework in the JavaScript and TypeScript ecosystem. It starts zero-config and ships with built-in assertions, mocking and coverage. This article walks through every core Jest feature and modern testing practice.

Installation

npm i -D jest @types/jest
npm i -D ts-jest typescript  # for TS projects

# package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

# For TypeScript
npx ts-jest config:init

Your First Test

// math.js
function add(a, b) { return a + b; }
module.exports = { add };

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

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

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

    it('works with zero', () => {
        expect(add(0, 5)).toBe(5);
    });
});
npm test
# PASS  ./math.test.js
#   add()
#     ✓ adds two positive numbers (2 ms)
#     ✓ adds negative numbers
#     ✓ works with zero

Matchers

// Equality
expect(x).toBe(5);                // primitive === 
expect(obj).toEqual({ a: 1 });    // deep equal
expect(obj).toStrictEqual({ a: 1 });  // + type check

// Truthy/Falsy
expect(x).toBeTruthy();
expect(x).toBeNull();
expect(x).toBeDefined();

// Numbers
expect(x).toBeGreaterThan(3);
expect(x).toBeCloseTo(0.3);       // float

// String / Array
expect(s).toMatch(/error/i);
expect(arr).toContain('apple');
expect(arr).toHaveLength(3);

// Objects
expect(user).toHaveProperty('email');
expect(user).toMatchObject({ name: 'Alex' });  // subset

// Exceptions
expect(() => throwError()).toThrow('Bad input');

// Negation
expect(x).not.toBe(0);

Setup and Teardown

describe('User service', () => {
    let db;

    beforeAll(async () => {
        db = await connectDB();
    });

    afterAll(async () => {
        await db.close();
    });

    beforeEach(async () => {
        await db.clear();
    });

    it('creates user', async () => {
        const user = await createUser(db, { email: 'a@b.com' });
        expect(user.id).toBeDefined();
    });
});

Async Tests

// With a promise
it('fetches user', () => {
    return getUser(1).then(user => {
        expect(user.name).toBe('Alex');
    });
});

// async/await (preferred)
it('fetches user', async () => {
    const user = await getUser(1);
    expect(user.name).toBe('Alex');
});

// Expecting a rejection
it('throws on invalid id', async () => {
    await expect(getUser(-1)).rejects.toThrow('Invalid');
});

// Expecting a resolution
it('resolves with data', async () => {
    await expect(getUser(1)).resolves.toEqual(expect.objectContaining({ name: 'Alex' }));
});

Mock Functions

// Jest mock
const logger = jest.fn();
logger('hello');
logger('world');

expect(logger).toHaveBeenCalled();
expect(logger).toHaveBeenCalledTimes(2);
expect(logger).toHaveBeenCalledWith('hello');
expect(logger).toHaveBeenNthCalledWith(1, 'hello');

// Mock return values
const fetchUser = jest.fn().mockResolvedValue({ id: 1, name: 'Alex' });
const errFetch = jest.fn().mockRejectedValue(new Error('Network'));

// Implementation
const add = jest.fn((a, b) => a + b);
expect(add(2, 3)).toBe(5);

Module Mocks

// __mocks__/axios.js
module.exports = {
    get: jest.fn(),
    post: jest.fn()
};

// user.test.js
jest.mock('axios');
const axios = require('axios');

it('fetches user', async () => {
    axios.get.mockResolvedValue({ data: { name: 'Alex' } });
    const user = await getUser(1);
    expect(user).toEqual({ name: 'Alex' });
    expect(axios.get).toHaveBeenCalledWith('/users/1');
});

Spies

// Observe an existing function while still calling it
const spy = jest.spyOn(console, 'log');
myFunction();
expect(spy).toHaveBeenCalledWith('doing work');
spy.mockRestore();  // revert to the original

Snapshot Testing

it('renders correctly', () => {
    const { container } = render(<Button label="Save" />);
    expect(container).toMatchSnapshot();
});
// First run creates the snapshot
// Subsequent runs compare — a diff fails the test
// Accept with: jest -u (update)

Coverage

npm test -- --coverage

# jest.config.js
module.exports = {
    coverageDirectory: 'coverage',
    collectCoverageFrom: ['src/**/*.{js,ts}', '!**/*.test.{js,ts}'],
    coverageThreshold: {
        global: { branches: 80, functions: 80, lines: 80, statements: 80 }
    }
};

Test Driven Development (TDD)

  • Red: write a failing test (the function doesn't exist yet)
  • Green: write the simplest code to make the test pass
  • Refactor: clean up the code, the test still passes
  • Repeat

Vitest as an Alternative

If you're on a Vite project, consider Vitest instead of Jest. The API is 95% Jest-compatible, it's 3-5x faster, ESM-native and first-class TS support.

Common Pitfalls

  • Forgetting await in async tests → false positives
  • Not clearing the DB between tests → flaky tests
  • Overly large snapshots → impossible to review
  • Testing implementation details → breaks on refactor
  • No test isolation → test order affects results

Conclusion

A codebase without tests is a vault of future surprises. With Jest, a one-hour setup gives you a high-standard safety net — aim for 80%+ coverage on critical business logic. Combine with React Testing Library for the UI and Playwright for E2E to get the full testing pyramid.

Testing infrastructure setup

Reach out to KEYDAL for a Jest, Vitest or Playwright testing pyramid and CI integration. Contact us

WhatsApp