You write a utility function. It works. You test it manually. It works. Ship it. Then bugs appear. Edge cases you missed. Null values crash it. Empty arrays break it. Users complain.

You add more features. Change existing functions. Break something else. No tests to catch regressions. Every change is risky. You manually test everything. Slow. Tedious. Error-prone.

You try writing tests. Don't know where to start. Which functions to test? What to test? How to structure tests? Tests feel like wasted time. Code works, why test?

Tests are not wasted time. Tests catch bugs before users do. Tests enable confident refactoring. Tests document expected behavior. Tests make development faster, not slower. Jest makes testing JavaScript easy.

QUICK SUMMARY

What You'll Learn: Jest is a JavaScript testing framework for unit tests. Unit tests verify individual functions work correctly. Write tests with test() or it(), make assertions with expect(). Common matchers: toBe (strict equality), toEqual (deep equality), toBeTruthy, toContain, toThrow. Test async functions with async/await or returning promises. Mock functions with jest.fn() to track calls and control return values. Test edge cases: null, undefined, empty arrays, large numbers. Use describe() to group related tests. beforeEach() runs setup before each test. Test coverage shows which code is tested. Good tests are isolated, fast, and test one thing. TDD (Test-Driven Development) writes tests before code. Tests catch regressions and enable refactoring.

Why It Matters:

  • Catch bugs before production
  • Enable confident refactoring
  • Document expected behavior
  • Prevent regressions
  • Faster development long-term
  • Better code design
  • Industry standard practice

Key Concepts:

  • test() or it() defines tests
  • expect() makes assertions
  • Matchers (toBe, toEqual, toContain)
  • Testing async code
  • Mock functions with jest.fn()
  • describe() groups tests
  • beforeEach/afterEach setup
  • Test coverage reports

What You'll Build:

  • Unit tests for utilities
  • Async function tests
  • Mock function tests
  • Edge case tests
  • Error handling tests
  • Complete test suite
  • TDD examples

Time Investment: Basic Jest takes 1–2 hours to learn. Writing good tests takes practice.

Prerequisites:

  • JavaScript fundamentals
  • Functions and modules
  • Async/await understanding
  • Basic testing concepts

FULL DETAILED ARTICLE

Installing Jest

# Install Jest
npm install --save-dev jest
# Add test script to package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}
# Run tests
npm test

Your First Test

// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});
test('adds negative numbers', () => {
  expect(sum(-1, -2)).toBe(-3);
});
test('adds zero', () => {
  expect(sum(5, 0)).toBe(5);
});

Run tests:

npm test
# Output:
# PASS  sum.test.js
#   ✓ adds 1 + 2 to equal 3
#   ✓ adds negative numbers
#   ✓ adds zero

Common Matchers

Equality matchers:

test('equality matchers', () => {
  // toBe - strict equality (===)
  expect(2 + 2).toBe(4);
  expect('hello').toBe('hello');
  
  // not.toBe
  expect(2 + 2).not.toBe(5);
  
  // toEqual - deep equality (for objects/arrays)
  expect({ name: 'John' }).toEqual({ name: 'John' });
  expect([1, 2, 3]).toEqual([1, 2, 3]);
  
  // toStrictEqual - strict deep equality (checks undefined values)
  expect({ a: 1, b: undefined }).toStrictEqual({ a: 1, b: undefined });
});

Truthiness matchers:

test('truthiness matchers', () => {
  expect(true).toBeTruthy();
  expect(1).toBeTruthy();
  expect('hello').toBeTruthy();
  
  expect(false).toBeFalsy();
  expect(0).toBeFalsy();
  expect('').toBeFalsy();
  expect(null).toBeFalsy();
  expect(undefined).toBeFalsy();
  
  expect(null).toBeNull();
  expect(undefined).toBeUndefined();
  expect(5).toBeDefined();
});

Number matchers:

test('number matchers', () => {
  expect(2 + 2).toBeGreaterThan(3);
  expect(2 + 2).toBeGreaterThanOrEqual(4);
  expect(2 + 2).toBeLessThan(5);
  expect(2 + 2).toBeLessThanOrEqual(4);
  
  // Floating point equality
  expect(0.1 + 0.2).toBeCloseTo(0.3);
});

String matchers:

test('string matchers', () => {
  expect('Hello World').toMatch(/World/);
  expect('Hello World').toMatch('World');
  expect('testing').not.toMatch(/xyz/);
  
  expect('hello@example.com').toContain('@');
  expect('JavaScript').toContain('Script');
});

Array and iterable matchers:

test('array matchers', () => {
  const arr = [1, 2, 3, 4, 5];
  
  expect(arr).toContain(3);
  expect(arr).toHaveLength(5);
  expect(arr).toEqual(expect.arrayContaining([2, 3]));
  
  expect(['apple', 'banana', 'orange']).toContain('banana');
});

Object matchers:

test('object matchers', () => {
  const user = {
    name: 'John',
    age: 30,
    email: 'john@example.com'
  };
  
  expect(user).toHaveProperty('name');
  expect(user).toHaveProperty('name', 'John');
  expect(user).toMatchObject({ name: 'John', age: 30 });
  
  expect(user).toEqual(
    expect.objectContaining({
      name: expect.any(String),
      age: expect.any(Number)
    })
  );
});

Testing Functions

Pure function testing:

// utils.js
function capitalize(str) {
  if (!str) return '';
  return str.charAt(0).toUpperCase() + str.slice(1);
}
function isEmail(str) {
  return /^\S+@\S+\.\S+$/.test(str);
}
function filterEven(numbers) {
  return numbers.filter(n => n % 2 === 0);
}
module.exports = { capitalize, isEmail, filterEven };
// utils.test.js
const { capitalize, isEmail, filterEven } = require('./utils');
describe('capitalize', () => {
  test('capitalizes first letter', () => {
    expect(capitalize('hello')).toBe('Hello');
  });
  
  test('handles empty string', () => {
    expect(capitalize('')).toBe('');
  });
  
  test('handles null/undefined', () => {
    expect(capitalize(null)).toBe('');
    expect(capitalize(undefined)).toBe('');
  });
  
  test('preserves rest of string', () => {
    expect(capitalize('hello world')).toBe('Hello world');
  });
});
describe('isEmail', () => {
  test('validates correct email', () => {
    expect(isEmail('test@example.com')).toBe(true);
  });
  
  test('rejects invalid email', () => {
    expect(isEmail('invalid')).toBe(false);
    expect(isEmail('test@')).toBe(false);
    expect(isEmail('@example.com')).toBe(false);
  });
});
describe('filterEven', () => {
  test('filters even numbers', () => {
    expect(filterEven([1, 2, 3, 4, 5])).toEqual([2, 4]);
  });
  
  test('handles empty array', () => {
    expect(filterEven([])).toEqual([]);
  });
  
  test('handles all odd', () => {
    expect(filterEven([1, 3, 5])).toEqual([]);
  });
  
  test('handles all even', () => {
    expect(filterEven([2, 4, 6])).toEqual([2, 4, 6]);
  });
});

Testing Async Code

With promises:

// api.js
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return await response.json();
}
// api.test.js
test('fetches user data', async () => {
  const user = await fetchUser(1);
  expect(user).toHaveProperty('name');
});
// Or return promise
test('fetches user data', () => {
  return fetchUser(1).then(user => {
    expect(user).toHaveProperty('name');
  });
});
// Test rejection
test('handles fetch error', async () => {
  await expect(fetchUser(999)).rejects.toThrow();
});

With callbacks:

function fetchData(callback) {
  setTimeout(() => {
    callback('data');
  }, 100);
}
test('fetches data', (done) => {
  function callback(data) {
    expect(data).toBe('data');
    done();  // Tell Jest test is complete
  }
  
  fetchData(callback);
});

Resolves and rejects matchers:

test('resolves to data', async () => {
  await expect(fetchUser(1)).resolves.toHaveProperty('name');
});
test('rejects with error', async () => {
  await expect(fetchUser(999)).rejects.toThrow('Not found');
});

Mock Functions

Create mock functions:

test('mock function', () => {
  const mockFn = jest.fn();
  
  mockFn();
  mockFn(1, 2);
  mockFn('hello');
  
  // Check if called
  expect(mockFn).toHaveBeenCalled();
  
  // Check call count
  expect(mockFn).toHaveBeenCalledTimes(3);
  
  // Check arguments
  expect(mockFn).toHaveBeenCalledWith(1, 2);
  expect(mockFn).toHaveBeenLastCalledWith('hello');
  
  // Check all calls
  expect(mockFn.mock.calls).toEqual([
    [],
    [1, 2],
    ['hello']
  ]);
});

Mock return values:

test('mock return values', () => {
  const mockFn = jest.fn();
  
  // Set return value
  mockFn.mockReturnValue(42);
  expect(mockFn()).toBe(42);
  
  // Different returns
  mockFn.mockReturnValueOnce(1)
        .mockReturnValueOnce(2)
        .mockReturnValue(3);
  
  expect(mockFn()).toBe(1);
  expect(mockFn()).toBe(2);
  expect(mockFn()).toBe(3);
  expect(mockFn()).toBe(3);  // Default
});
test('mock async return', async () => {
  const mockFn = jest.fn();
  
  mockFn.mockResolvedValue({ id: 1, name: 'John' });
  
  const result = await mockFn();
  expect(result).toEqual({ id: 1, name: 'John' });
});

Mock implementations:

test('mock implementation', () => {
  const mockFn = jest.fn(x => x * 2);
  
  expect(mockFn(5)).toBe(10);
  expect(mockFn).toHaveBeenCalledWith(5);
});
test('mock different implementations', () => {
  const mockFn = jest.fn();
  
  mockFn.mockImplementationOnce(x => x + 1)
        .mockImplementationOnce(x => x + 2);
  
  expect(mockFn(5)).toBe(6);
  expect(mockFn(5)).toBe(7);
});

Testing with mocks:

// calculator.js
function calculate(a, b, operation) {
  return operation(a, b);
}
// calculator.test.js
test('calls operation function', () => {
  const mockOperation = jest.fn((a, b) => a + b);
  
  const result = calculate(5, 3, mockOperation);
  
  expect(result).toBe(8);
  expect(mockOperation).toHaveBeenCalledWith(5, 3);
  expect(mockOperation).toHaveBeenCalledTimes(1);
});

Mocking Modules

Mock entire module:

// api.js
export async function fetchUsers() {
  const response = await fetch('/api/users');
  return await response.json();
}
// userService.js
import { fetchUsers } from './api';
export async function getAllUsers() {
  const users = await fetchUsers();
  return users.filter(u => u.active);
}
// userService.test.js
import { getAllUsers } from './userService';
import { fetchUsers } from './api';
// Mock the entire module
jest.mock('./api');
test('filters active users', async () => {
  // Set mock return value
  fetchUsers.mockResolvedValue([
    { id: 1, name: 'User 1', active: true },
    { id: 2, name: 'User 2', active: false },
    { id: 3, name: 'User 3', active: true }
  ]);
  
  const users = await getAllUsers();
  
  expect(users).toHaveLength(2);
  expect(users).toEqual([
    { id: 1, name: 'User 1', active: true },
    { id: 3, name: 'User 3', active: true }
  ]);
});

Mock specific functions:

jest.mock('./api', () => ({
  fetchUsers: jest.fn(),
  fetchUser: jest.fn()
}));

Mock modules in separate file:

// __mocks__/api.js
export const fetchUsers = jest.fn(() => Promise.resolve([]));
export const fetchUser = jest.fn(() => Promise.resolve({}));
// test file
jest.mock('./api');  // Automatically uses __mocks__/api.js

Setup and Teardown

describe('User tests', () => {
  let users;
  
  // Runs before each test
  beforeEach(() => {
    users = [
      { id: 1, name: 'User 1' },
      { id: 2, name: 'User 2' }
    ];
  });
  
  // Runs after each test
  afterEach(() => {
    users = null;
  });
  
  // Runs once before all tests
  beforeAll(() => {
    console.log('Starting tests');
  });
  
  // Runs once after all tests
  afterAll(() => {
    console.log('Tests complete');
  });
  
  test('has users', () => {
    expect(users).toHaveLength(2);
  });
  
  test('can add user', () => {
    users.push({ id: 3, name: 'User 3' });
    expect(users).toHaveLength(3);
  });
});

Testing Error Handling

function divide(a, b) {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}
test('throws error on divide by zero', () => {
  expect(() => divide(10, 0)).toThrow();
  expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
  expect(() => divide(10, 0)).toThrow(Error);
});
test('does not throw on valid input', () => {
  expect(() => divide(10, 2)).not.toThrow();
});

Async error handling:

async function fetchUser(id) {
  if (id < 0) {
    throw new Error('Invalid ID');
  }
  // fetch user...
}
test('throws on invalid ID', async () => {
  await expect(fetchUser(-1)).rejects.toThrow('Invalid ID');
});

Testing Edge Cases

describe('edge cases', () => {
  test('handles null', () => {
    expect(processValue(null)).toBe(null);
  });
  
  test('handles undefined', () => {
    expect(processValue(undefined)).toBe(undefined);
  });
  
  test('handles empty string', () => {
    expect(processValue('')).toBe('');
  });
  
  test('handles empty array', () => {
    expect(processArray([])).toEqual([]);
  });
  
  test('handles empty object', () => {
    expect(processObject({})).toEqual({});
  });
  
  test('handles very large numbers', () => {
    expect(sum(Number.MAX_SAFE_INTEGER, 1)).toBeDefined();
  });
  
  test('handles negative numbers', () => {
    expect(abs(-5)).toBe(5);
  });
  
  test('handles special characters', () => {
    expect(sanitize('<script>alert("xss")</script>')).not.toContain('<script>');
  });
});

Test Coverage

# Run with coverage
npm test -- --coverage
# Output:
# ----------------------|---------|----------|---------|---------|
# File                  | % Stmts | % Branch | % Funcs | % Lines |
# ----------------------|---------|----------|---------|---------|
# All files             |   85.71 |    66.67 |     100 |   85.71 |
#  sum.js               |     100 |      100 |     100 |     100 |
#  utils.js             |      75 |       50 |     100 |      75 |
# ----------------------|---------|----------|---------|---------|

Coverage thresholds:

// jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Test-Driven Development (TDD)

Write test first, then implementation:

// 1. Write test (RED - fails)
test('formats phone number', () => {
  expect(formatPhone('1234567890')).toBe('(123) 456-7890');
});
// 2. Write minimal code to pass (GREEN)
function formatPhone(phone) {
  return `(${phone.slice(0, 3)}) ${phone.slice(3, 6)}-${phone.slice(6)}`;
}
// 3. Refactor (improve while keeping tests green)
function formatPhone(phone) {
  if (!phone || phone.length !== 10) return '';
  const area = phone.slice(0, 3);
  const prefix = phone.slice(3, 6);
  const line = phone.slice(6);
  return `(${area}) ${prefix}-${line}`;
}
// 4. Add more tests
test('handles invalid input', () => {
  expect(formatPhone('')).toBe('');
  expect(formatPhone(null)).toBe('');
  expect(formatPhone('123')).toBe('');
});

Organizing Tests

describe('MathUtils', () => {
  describe('addition', () => {
    test('adds positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });
    
    test('adds negative numbers', () => {
      expect(add(-2, -3)).toBe(-5);
    });
  });
  
  describe('multiplication', () => {
    test('multiplies positive numbers', () => {
      expect(multiply(2, 3)).toBe(6);
    });
    
    test('multiplies by zero', () => {
      expect(multiply(5, 0)).toBe(0);
    });
  });
});

Skipping and Only

// Skip test
test.skip('skipped test', () => {
  // This won't run
});
// Run only this test
test.only('only this test', () => {
  // Only this runs
});
// Skip entire suite
describe.skip('skipped suite', () => {
  // None of these run
});

Parameterized Tests

// Test multiple cases
test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('adds %i + %i to equal %i', (a, b, expected) => {
  expect(a + b).toBe(expected);
});
// With objects
test.each([
  { a: 1, b: 1, expected: 2 },
  { a: 1, b: 2, expected: 3 },
  { a: 2, b: 1, expected: 3 },
])('adds $a + $b to equal $expected', ({ a, b, expected }) => {
  expect(a + b).toBe(expected);
});

Common Patterns

Pattern 1: Test Helper Functions

// testHelpers.js
export function createMockUser(overrides = {}) {
  return {
    id: 1,
    name: 'Test User',
    email: 'test@example.com',
    ...overrides
  };
}
// Use in tests
import { createMockUser } from './testHelpers';
test('processes user', () => {
  const user = createMockUser({ name: 'John' });
  expect(processUser(user)).toBeDefined();
});

Pattern 2: Snapshot Testing

test('object matches snapshot', () => {
  const data = {
    id: 1,
    name: 'Test',
    createdAt: new Date('2024-01-01')
  };
  
  expect(data).toMatchSnapshot();
});
// Update snapshots: npm test -- -u

Pattern 3: Custom Matchers

expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        `expected ${received} to be within range ${floor} - ${ceiling}`
    };
  }
});
test('custom matcher', () => {
  expect(100).toBeWithinRange(90, 110);
});

Common Mistakes

Mistake 1: Testing Implementation

// Bad - testing internals
test('calls internal method', () => {
  const obj = new MyClass();
  obj.internalMethod();
  expect(obj._privateVar).toBe(5);
});
// Good - testing behavior
test('produces correct output', () => {
  const obj = new MyClass();
  expect(obj.publicMethod()).toBe(expectedResult);
});

Mistake 2: Dependent Tests

// Bad - tests depend on each other
let user;
test('creates user', () => {
  user = createUser();
  expect(user).toBeDefined();
});
test('updates user', () => {
  updateUser(user);  // Fails if first test fails
  expect(user.updated).toBe(true);
});
// Good - independent tests
test('creates user', () => {
  const user = createUser();
  expect(user).toBeDefined();
});
test('updates user', () => {
  const user = createUser();
  updateUser(user);
  expect(user.updated).toBe(true);
});

Mistake 3: Not Testing Edge Cases

// Bad - only happy path
test('adds numbers', () => {
  expect(add(2, 3)).toBe(5);
});
// Good - test edge cases
test('adds positive numbers', () => {
  expect(add(2, 3)).toBe(5);
});
test('adds negative numbers', () => {
  expect(add(-2, -3)).toBe(-5);
});
test('adds zero', () => {
  expect(add(0, 5)).toBe(5);
});
test('handles NaN', () => {
  expect(add(NaN, 5)).toBe(NaN);
});

Practice Challenges

Test these utilities:

Challenge 1: String Utilities Test trim, capitalize, reverse, isPalindrome functions.

Challenge 2: Array Utilities Test unique, flatten, groupBy, partition functions.

Challenge 3: Date Utilities Test formatDate, addDays, diffDays, isWeekend functions.

Challenge 4: Validation Functions Test email, phone, password, credit card validators.

Challenge 5: Async Queue Test queue that processes items asynchronously.

Summary: Your Jest Testing Checklist

Basic Tests:

  • Use test() or it() for tests
  • Use expect() for assertions
  • Use describe() to group tests
  • Test one thing per test

Matchers:

  • toBe for primitives
  • toEqual for objects/arrays
  • toContain for arrays/strings
  • toThrow for errors
  • toHaveBeenCalled for mocks

Async Testing:

  • Use async/await
  • Return promises
  • Use resolves/rejects
  • Don't forget done callback

Mocking:

  • jest.fn() for mock functions
  • jest.mock() for modules
  • mockReturnValue/mockResolvedValue
  • Track calls with toHaveBeenCalled

Best Practices:

  • Test behavior, not implementation
  • Test edge cases
  • Keep tests isolated
  • Use beforeEach for setup
  • Aim for high coverage
  • Write tests first (TDD)

Helpful Resources

Documentation:

  • Jest Documentation
  • Jest API Reference
  • Jest Matchers

Tools:

  • Jest VS Code Extension
  • Wallaby.js — Test runner
  • Majestic — Jest GUI

Previous Posts:

  • Post 61: React Testing Library

You are 62 posts into the 75-post series. Jest unit testing is now in your toolkit.