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
awaitin 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.
Reach out to KEYDAL for a Jest, Vitest or Playwright testing pyramid and CI integration. Contact us