{ } JEST • TESTING • TDD
Testing

Unit Testing in JavaScript with Jest

Mayur Dabhi
Mayur Dabhi
March 27, 2026
22 min read

Jest is a delightful JavaScript testing framework developed by Facebook (now Meta) that focuses on simplicity and developer experience. Whether you're testing React components, Node.js backends, or vanilla JavaScript libraries, Jest provides everything you need out of the box—zero configuration required for most projects.

Unit testing is essential for maintaining code quality, catching bugs early, and enabling confident refactoring. In this comprehensive guide, we'll explore Jest from the fundamentals to advanced techniques, complete with practical examples and best practices you can apply immediately.

What You'll Learn
  • Setting up Jest in JavaScript and TypeScript projects
  • Writing effective test cases with describe and it blocks
  • Mastering Jest matchers for all assertion types
  • Mocking functions, modules, and API calls
  • Testing asynchronous code with async/await
  • Snapshot testing for UI components
  • Code coverage configuration and best practices
  • Test-Driven Development (TDD) workflow

Why Jest?

Jest has become the de facto standard for JavaScript testing, and for good reason. It offers a complete testing solution with built-in assertions, mocking, code coverage, and a powerful CLI. Unlike older testing setups that required combining multiple libraries, Jest provides everything in a single package.

Jest vs Traditional Testing Stack Traditional Setup Mocha (Test Runner) Chai (Assertions) Sinon (Mocking) Istanbul (Coverage) Jest (All-in-One) Test Runner Built-in Assertions Powerful Mocking Code Coverage Snapshot Testing JEST

Jest replaces an entire testing stack with a single, cohesive solution

Zero Config

Works out of the box for most JavaScript projects. Just install and run.

Fast Parallel Testing

Tests run in parallel in separate processes for maximum performance.

Snapshot Testing

Capture component output and detect unexpected changes automatically.

Watch Mode

Intelligently runs only tests related to changed files during development.

Getting Started with Jest

Let's set up Jest in a new project. The installation process is straightforward, and Jest works immediately with sensible defaults.

Installation

Terminal
# Create a new project
mkdir my-project && cd my-project
npm init -y

# Install Jest
npm install --save-dev jest

# For TypeScript projects
npm install --save-dev jest @types/jest ts-jest typescript

Configuration

Add a test script to your package.json:

package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "devDependencies": {
    "jest": "^29.7.0"
  }
}
Pro Tip

For most projects, Jest's defaults work great. You only need a jest.config.js file for custom configurations like TypeScript support, custom test environments, or module aliases.

Writing Your First Test

Jest follows a simple naming convention: test files should be named *.test.js or *.spec.js, or placed in a __tests__ directory. Let's write our first test:

math.js
// A simple module to test
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

module.exports = { add, subtract, multiply, divide };
math.test.js
const { add, subtract, multiply, divide } = require('./math');

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

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

    it('should handle zero', () => {
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('divide', () => {
    it('should divide two numbers', () => {
      expect(divide(10, 2)).toBe(5);
    });

    it('should throw error when dividing by zero', () => {
      expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
    });
  });
});

Run your tests with:

$ npm test

PASS ./math.test.js
Math functions
add
✓ should add two positive numbers (2 ms)
✓ should add negative numbers
✓ should handle zero
divide
✓ should divide two numbers (1 ms)
✓ should throw error when dividing by zero

Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Time: 0.892 s

Jest Matchers Deep Dive

Matchers are the heart of Jest assertions. They let you test values in different ways. Jest provides a rich set of matchers for virtually any testing scenario.

Jest Matchers Categories Common Matchers .toBe() .toEqual() .toBeNull() .toBeUndefined() .toBeDefined() Truthiness .toBeTruthy() .toBeFalsy() .not.toBe() .toBeNull() .toBeNaN() Numbers .toBeGreaterThan() .toBeLessThan() .toBeGreaterThanOrEqual() .toBeCloseTo() .toBeLessThanOrEqual() Strings .toMatch(/regex/) .toContain() .toHaveLength() .toMatchSnapshot() .toMatchInlineSnapshot() Arrays & Objects .toContain() .toContainEqual() .toHaveProperty() .toMatchObject() .toHaveLength() Exceptions .toThrow() .toThrowError() .toThrowErrorMatchingSnapshot() .rejects.toThrow() .resolves.toBe()

Jest provides matchers for every testing scenario

Common Matchers Examples

// toBe uses Object.is (strict equality)
expect(2 + 2).toBe(4);
expect('hello').toBe('hello');

// toEqual recursively checks object equality
expect({ name: 'John', age: 30 }).toEqual({ name: 'John', age: 30 });
expect([1, 2, 3]).toEqual([1, 2, 3]);

// toStrictEqual also checks for undefined properties
expect({ a: undefined, b: 2 }).not.toStrictEqual({ b: 2 });
// Truthiness matchers
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();

// Boolean-like checks
expect(1).toBeTruthy();
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect([]).toBeTruthy(); // Arrays are truthy!

// Negation with .not
expect(5).not.toBeNull();
expect('test').not.toBeFalsy();
// Numeric comparisons
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(5).toBeLessThanOrEqual(5);

// Floating point numbers (avoid toBe due to precision)
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);

// Check for NaN
expect(NaN).toBeNaN();
expect(Number('abc')).toBeNaN();
// Array matchers
const fruits = ['apple', 'banana', 'orange'];

expect(fruits).toContain('banana');
expect(fruits).toHaveLength(3);
expect(fruits).not.toContain('grape');

// Check for object in array
const users = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
expect(users).toContainEqual({ id: 1, name: 'John' });

// Object property checks
const user = { name: 'John', address: { city: 'NYC' } };
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('address.city', 'NYC');

Mocking with Jest

Mocking is essential for isolating the code under test from external dependencies. Jest provides powerful mocking capabilities that let you replace functions, modules, and timers with controlled implementations.

Mocking Architecture Without Mocking Your Code Real API Network (slow) With Mocking Your Code Mock (instant) ✓ Fast ✓ Reliable ✓ Controlled Types of Mocks jest.fn() Mock individual functions jest.mock() Mock entire modules jest.spyOn() Spy on existing methods jest.useFakeTimers() Mock setTimeout, setInterval

Function Mocks

mocking.test.js
// Creating a mock function
const mockCallback = jest.fn(x => x + 10);

[1, 2, 3].forEach(mockCallback);

// Checking how the mock was called
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith(1);
expect(mockCallback).toHaveBeenLastCalledWith(3);

// Checking return values
expect(mockCallback.mock.results[0].value).toBe(11);
expect(mockCallback.mock.results[1].value).toBe(12);

// Mock implementation
const mockFn = jest.fn()
  .mockReturnValueOnce(10)
  .mockReturnValueOnce(20)
  .mockReturnValue(30);

console.log(mockFn()); // 10
console.log(mockFn()); // 20
console.log(mockFn()); // 30
console.log(mockFn()); // 30

Module Mocking

userService.test.js
// userService.js
const axios = require('axios');

async function getUser(id) {
  const response = await axios.get(`/api/users/${id}`);
  return response.data;
}

// Mock axios module
jest.mock('axios');

describe('getUser', () => {
  it('should fetch user by id', async () => {
    const mockUser = { id: 1, name: 'John Doe' };
    
    // Setup mock response
    axios.get.mockResolvedValue({ data: mockUser });
    
    const user = await getUser(1);
    
    expect(axios.get).toHaveBeenCalledWith('/api/users/1');
    expect(user).toEqual(mockUser);
  });

  it('should handle errors', async () => {
    axios.get.mockRejectedValue(new Error('Network error'));
    
    await expect(getUser(1)).rejects.toThrow('Network error');
  });
});

Testing Asynchronous Code

Modern JavaScript is inherently asynchronous. Jest provides several ways to test async code, including callbacks, Promises, and async/await syntax.

Async/Await Pattern (Recommended)

// Using async/await - cleanest syntax
describe('Async operations', () => {
  it('should fetch data successfully', async () => {
    const data = await fetchData();
    expect(data).toBeDefined();
    expect(data.status).toBe('success');
  });

  it('should handle async errors', async () => {
    await expect(fetchBadData()).rejects.toThrow('Not found');
  });

  it('should resolve with expected value', async () => {
    await expect(Promise.resolve('value')).resolves.toBe('value');
  });
});

Promise Pattern

// Using Promises - return the promise
it('should resolve with data', () => {
  return fetchData().then(data => {
    expect(data.status).toBe('success');
  });
});

// Using .resolves/.rejects
it('should resolve with value', () => {
  return expect(fetchData()).resolves.toMatchObject({
    status: 'success'
  });
});

it('should reject with error', () => {
  return expect(failingRequest()).rejects.toThrow();
});

Callback Pattern (Legacy)

// Using done callback - for legacy callback-based APIs
it('should call the callback with data', done => {
  function callback(error, data) {
    try {
      expect(error).toBeNull();
      expect(data).toBe('success');
      done(); // Call done() when finished
    } catch (e) {
      done(e); // Pass error to done
    }
  }

  fetchWithCallback(callback);
});

Snapshot Testing

Snapshot testing captures the output of your code and compares it against a stored reference. It's particularly useful for testing UI components, but can also be used for any serializable output.

component.test.js
import { render } from '@testing-library/react';
import UserCard from './UserCard';

describe('UserCard', () => {
  it('should match snapshot', () => {
    const user = { name: 'John', role: 'Developer' };
    const { container } = render(<UserCard user={user} />);
    
    // Creates/compares snapshot file
    expect(container).toMatchSnapshot();
  });

  it('should match inline snapshot', () => {
    const user = { name: 'Jane', email: 'jane@test.com' };
    
    // Snapshot stored inline in test file
    expect(user).toMatchInlineSnapshot(`
      Object {
        "email": "jane@test.com",
        "name": "Jane",
      }
    `);
  });
});
Snapshot Best Practices
  • Review snapshot changes carefully before committing
  • Keep snapshots small and focused
  • Use inline snapshots for small, stable outputs
  • Don't snapshot large or frequently changing data
  • Update snapshots with jest --updateSnapshot when intentional

Code Coverage

Code coverage shows how much of your code is executed during tests. Jest has built-in coverage reporting that identifies untested code paths.

jest.config.js
module.exports = {
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.test.{js,jsx,ts,tsx}',
    '!src/index.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Run tests with coverage:

$ npm test -- --coverage

----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files | 92.5 | 88.2 | 95.0 | 92.1 |
math.js | 100 | 100 | 100 | 100 |
user.js | 85.0 | 76.5 | 90.0 | 84.2 |
----------|---------|----------|---------|---------|

Understanding Coverage Metrics

Statements: % of statements executed

92%

Branches: % of if/else branches taken

85%

Functions: % of functions called

95%

Lines: % of lines executed

90%

Test-Driven Development (TDD)

Test-Driven Development is a software development approach where you write tests before writing the actual code. The TDD cycle follows three steps: Red (write a failing test), Green (write minimal code to pass), and Refactor (improve the code).

TDD Cycle: Red → Green → Refactor RED Write failing test GREEN Make it pass REFACTOR Improve code

TDD in Practice

1

Write a Failing Test (Red)

Start by writing a test for the behavior you want to implement. Run the test—it should fail because the functionality doesn't exist yet.

2

Make It Pass (Green)

Write the minimum amount of code necessary to make the test pass. Don't worry about elegance or optimization at this stage.

3

Refactor

Now improve your code while keeping all tests green. Remove duplication, improve naming, optimize performance. The tests ensure you don't break anything.

Testing Best Practices

Practice Do Don't
Test Names Describe behavior: "should return empty array when no items" Vague names: "test1", "works"
Test Size One assertion per concept, small focused tests Giant tests with many unrelated assertions
Dependencies Mock external dependencies (APIs, databases) Test against real external services
Test Data Use factories or fixtures for consistent data Hardcode random data throughout tests
Isolation Each test should be independent Tests that depend on other tests' state
Coverage Focus on critical paths and edge cases Chase 100% coverage at all costs

The AAA Pattern

Structure your tests using the Arrange-Act-Assert pattern:

  • Arrange: Set up test data and conditions
  • Act: Execute the code being tested
  • Assert: Verify the results are as expected
aaa-pattern.test.js
describe('ShoppingCart', () => {
  it('should calculate total with discount', () => {
    // Arrange
    const cart = new ShoppingCart();
    cart.addItem({ name: 'Laptop', price: 1000 });
    cart.addItem({ name: 'Mouse', price: 50 });
    cart.applyDiscount(0.1); // 10% discount

    // Act
    const total = cart.calculateTotal();

    // Assert
    expect(total).toBe(945); // (1000 + 50) * 0.9
  });
});

Useful Jest CLI Options

Terminal Commands
# Run tests in watch mode
jest --watch

# Run only changed tests
jest --onlyChanged

# Run specific test file
jest user.test.js

# Run tests matching pattern
jest --testNamePattern="should validate"

# Run with coverage
jest --coverage

# Update snapshots
jest --updateSnapshot

# Run tests in band (sequentially)
jest --runInBand

# Verbose output
jest --verbose

# Clear cache
jest --clearCache

Conclusion

Jest has transformed JavaScript testing by providing a comprehensive, zero-configuration solution that scales from simple utility functions to complex React applications. Its powerful features—including mocking, snapshot testing, and built-in code coverage—make it the go-to choice for modern JavaScript development.

Key takeaways from this guide:

Testing isn't just about catching bugs—it's about building confidence in your code. With Jest in your toolkit, you have everything you need to write reliable, maintainable JavaScript applications. Start testing today, and your future self will thank you!

Next Steps

Ready to level up? Explore Jest's documentation for advanced features like custom matchers, testing timers, and integrating with TypeScript. Also check out Testing Library for testing React components with a user-centric approach.

Jest Testing JavaScript Unit Testing TDD Mocking
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and modern JavaScript.