Code without tests is broken by default. You just don't know it yet. Testing gives you confidence to refactor, ship features faster, and sleep peacefully knowing that a 2 AM deployment won't bring down production. This guide covers everything you need to start testing JavaScript effectively.
Types of Tests
- Unit Tests: Test individual functions or modules in isolation. Fast, cheap, and you should have lots of them.
- Integration Tests: Test how multiple units work together — API routes, database queries, component interactions.
- End-to-End (E2E) Tests: Test the full application from the user's perspective — clicking buttons, filling forms, navigating pages.
The Testing Pyramid
/ E2E Tests \ ← Few (slow, expensive)
/ Integration Tests \ ← Moderate
/ Unit Tests \ ← Many (fast, cheap)
Getting Started with Jest
Setup
# Install Jest
npm install -D jest
# For TypeScript
npm install -D jest ts-jest @types/jest
# Add test script to package.json
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
Writing Your First Test
math.js
export function add(a, b) {
return a + b;
}
export function divide(a, b) {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
}
math.test.js
import { add, divide } from './math';
describe('add', () => {
test('adds two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('handles negative numbers', () => {
expect(add(-1, -1)).toBe(-2);
});
test('handles zero', () => {
expect(add(0, 5)).toBe(5);
});
});
describe('divide', () => {
test('divides two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
});
});
Common Matchers
Jest Matchers
// Equality
expect(value).toBe(42); // Strict equality (===)
expect(obj).toEqual({ a: 1, b: 2 }); // Deep equality
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThanOrEqual(10);
expect(0.1 + 0.2).toBeCloseTo(0.3); // Floating point
// Strings
expect(str).toMatch(/regex/);
expect(str).toContain('substring');
// Arrays
expect(arr).toContain('item');
expect(arr).toHaveLength(3);
// Objects
expect(obj).toHaveProperty('name');
expect(obj).toMatchObject({ name: 'Alice' });
Mocking
Mocking Functions & Modules
// Mock a function
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ id: 1 }); // For async
// Verify calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledTimes(3);
// Mock a module
jest.mock('./database', () => ({
getUser: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
saveUser: jest.fn().mockResolvedValue(true),
}));
Testing Async Code
Async Tests
// async/await (recommended)
test('fetches user data', async () => {
const user = await getUser(1);
expect(user.name).toBe('Alice');
});
// Testing rejected promises
test('throws for invalid ID', async () => {
await expect(getUser(-1)).rejects.toThrow('Invalid ID');
});
Test-Driven Development (TDD)
- Red: Write a failing test first
- Green: Write the minimum code to make it pass
- Refactor: Improve the code while keeping tests green
TDD forces you to think about the interface before the implementation. The result is code that is inherently testable and well-designed.
Best Practices
- Test behavior, not implementation. If you refactor internals, tests shouldn't break.
- One assertion per test concept. Tests should fail for one reason only.
- Use descriptive test names.
"should return 404 when user not found"is better than"test getUser". - Aim for 80%+ code coverage but don't chase 100% — diminishing returns.
- Run tests in CI. Every pull request should pass all tests before merging.
- Keep tests fast. Slow tests don't get run. Mock external dependencies.
Try Our Free JavaScript Tools
Minify and beautify your JavaScript test files.