Skip to content

Unit Testing in JavaScript / TypeScript

Jest and Vitest are the dominant testing frameworks for JavaScript and TypeScript. Vitest is faster and integrates natively with Vite projects.

Terminal window
npm install -D jest @types/jest ts-jest

jest.config.ts:

export default {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
};
Terminal window
npm install -D vitest @vitest/ui jsdom

vite.config.ts:

import { defineConfig } from 'vite';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
},
});
src/calculator.ts
export function add(a: number, b: number): number {
return a + b;
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
src/calculator.test.ts
import { add, divide } from './calculator';
describe('Calculator', () => {
describe('add', () => {
it('returns the sum of two numbers', () => {
expect(add(2, 3)).toBe(5);
});
it.each([
[0, 5, 5],
[-1, 1, 0],
[100, -50, 50],
])('add(%i, %i) = %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
});
describe('divide', () => {
it('returns the quotient', () => {
expect(divide(10, 2)).toBe(5);
});
it('throws when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
expect(value).toBe(5); // strict equality (===)
expect(value).toEqual({ a: 1 }); // deep equality
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeGreaterThan(0);
expect(arr).toHaveLength(3);
expect(arr).toContain('apple');
expect(obj).toHaveProperty('name', 'Alice');
expect(str).toMatch(/regex/);
expect(fn).toThrow('error message');
import { fetchUser } from './api';
import { getUserProfile } from './userService';
jest.mock('./api'); // auto-mock the entire module
it('returns profile with user data', async () => {
// Set up the mock
(fetchUser as jest.Mock).mockResolvedValueOnce({
id: 1,
name: 'Alice',
});
const profile = await getUserProfile(1);
expect(profile.name).toBe('Alice');
expect(fetchUser).toHaveBeenCalledWith(1);
expect(fetchUser).toHaveBeenCalledTimes(1);
});
const sendEmail = jest.spyOn(emailService, 'send');
sendEmail.mockResolvedValue(undefined);
await userService.register({ email: 'alice@example.com' });
expect(sendEmail).toHaveBeenCalledWith(
expect.objectContaining({ to: 'alice@example.com' })
);
// Async/await
it('fetches user data', async () => {
const user = await userApi.getById(1);
expect(user.name).toBe('Alice');
});
// Promises
it('resolves with data', () => {
return userApi.getById(1).then(user => {
expect(user.name).toBe('Alice');
});
});
// Rejects / throws
it('rejects when user not found', async () => {
await expect(userApi.getById(999)).rejects.toThrow('User not found');
});
Terminal window
npm install -D @testing-library/react @testing-library/user-event
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
it('calls onSubmit with email and password', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await userEvent.type(screen.getByLabelText(/email/i), 'alice@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'secret123');
await userEvent.click(screen.getByRole('button', { name: /log in/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'secret123',
});
});
beforeAll(() => { /* run once before all tests */ });
afterAll(() => { /* run once after all tests */ });
beforeEach(() => { /* run before each test */ });
afterEach(() => { /* run after each test */ });
Terminal window
npx jest # run all tests
npx jest --watch # re-run on file changes
npx jest --coverage # with coverage report
npx jest user.test.ts # run specific file
npx jest -t "add" # run tests matching pattern