Munsif.
AboutExperienceProjectsAchievementsBlogsContact
HomeAboutExperienceProjectsAchievementsBlogsContact
Munsif.

Frontend Developer crafting scalable web applications with modern technologies and clean code practices.

Quick Links

  • About
  • Experience
  • Projects
  • Achievements

Connect

© 2026 Shaik Munsif. All rights reserved.

Built with Next.js & Tailwind

0%
Welcome back!Continue where you left off
Back to Blogs
Testing

Jest Unit Testing Mastery: The Complete Guide for React & Angular

Master Jest from setup to advanced patterns. Covers matchers, mocking (jest.fn, jest.mock, jest.spyOn), async testing, timers, snapshot testing, code coverage, and component testing for both React (Testing Library) and Angular (TestBed) with full interview preparation.

Feb 7, 202630 min read
JestTestingReactAngularUnit TestingInterview Prep
Testing MasteryPart 1 of 2
  • 1. Jest Unit Testing Mastery: The Complete Guide for React & Angular
  • 2. Angular Testing Mastery: The Complete Guide to TestBed, Mocking & Beyond

Introduction

Every developer writes code. But great developers write code they can prove works — and they do it with unit tests.

Unit testing is the practice of verifying that individual pieces of your code (functions, classes, components) work exactly as expected in isolation. Among the many testing frameworks available, Jest has become the undisputed standard. Created by Facebook, Jest powers the test suites at Facebook, Instagram, Twitter, Spotify, and thousands of other companies. It's the default testing framework for React (Create React App, Vite, Next.js) and is widely used in Angular projects alongside Karma.

Why Jest specifically? Because it's an all-in-one solution. Other testing setups require you to wire together a test runner (Mocha), an assertion library (Chai), a mocking library (Sinon), and a coverage tool (Istanbul) — each with its own API and quirks. Jest bundles everything into a single, cohesive package that just works out of the box. Zero configuration for most projects, blazing-fast parallel test execution, powerful mocking capabilities, built-in code coverage, and snapshot testing — it's all there.

This guide is your complete, intensive reference to Jest. We'll cover the fundamentals, progressive matchers, mocking strategies, async patterns, component testing for both React and Angular, and close with interview preparation. By the end, you'll be writing tests with confidence and speed.


What is Unit Testing?

Imagine you're building a house. Before the walls go up, an inspector checks each individual brick — is it the right size? Does it hold weight? Is it level? That's unit testing in software. You test each small piece (a function, a class, a component) in isolation before combining them into a full application.

A unit test is a small, automated piece of code that:

  1. Calls a specific function or component with known inputs
  2. Checks that the output matches what you expect
  3. Runs in milliseconds, giving you instant feedback
typescript
// You write a function
function add(a: number, b: number): number {
  return a + b;
}

// You write a test that PROVES it works
test('add(2, 3) should return 5', () => {
  expect(add(2, 3)).toBe(5); // ✅ Pass!
});

Why bother? Because manual testing is slow, unreliable, and doesn't scale. When your codebase has 100 functions, you can't manually re-test all of them every time you change one line. Unit tests run all 100 checks in seconds.

How This Guide is Structured

This guide progresses from simple to advanced. If you're a complete beginner, start from the top and work through each section. If you're already familiar with the basics, jump to the section you need:

SectionDifficultyWhat You'll Learn
Setting Up Jest🟢 BeginnerInstallation and configuration
Your First Test🟢 BeginnerAAA pattern, describe/it/expect
Matchers Deep Dive🟢 BeginnerAll assertion methods
Mocking Fundamentals🟡 Intermediatejest.fn, jest.mock, jest.spyOn
Mocking Modules & APIs🟡 IntermediateAxios, fetch, partial mocks
Testing Async Code🟡 IntermediatePromises, async/await, timers
Setup and Teardown🟡 IntermediatebeforeEach, afterEach, lifecycle
Code Coverage🟡 IntermediateCoverage reports and thresholds
Snapshot Testing🟡 IntermediateWhen and how to use snapshots
Testing React Components🟡 IntermediateReact Testing Library
Testing Angular Components🟡 IntermediateTestBed with Jest
Common Mistakes🔴 AdvancedPitfalls that trip everyone up
Interview Preparation🔴 AdvancedReal interview Q&A

Setting Up Jest

React Projects (Vite)

bash
# Install Jest with React Testing Library
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event

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

Create jest.config.ts:

typescript
export default {
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  moduleNameMapper: {
    '\\.(css|less|scss)$': 'identity-obj-proxy',
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  setupFilesAfterSetup: ['@testing-library/jest-dom'],  // Adds custom matchers like toBeInTheDocument()
};

Angular Projects

Angular CLI projects come with Karma + Jasmine by default, but you can switch to Jest for faster execution and a better developer experience:

bash
# Install Jest for Angular
npm install --save-dev jest @types/jest jest-preset-angular

# Remove Karma dependencies
npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter

Create jest.config.ts:

typescript
export default {
  preset: 'jest-preset-angular',
  setupFilesAfterSetup: ['<rootDir>/setup-jest.ts'],  // Initializes Angular testing environment
  testPathIgnorePatterns: ['/node_modules/', '/dist/'],
  moduleNameMapper: {
    '^@app/(.*)$': '<rootDir>/src/app/$1',
    '^@env/(.*)$': '<rootDir>/src/environments/$1',
  },
};

Create setup-jest.ts:

typescript
import 'jest-preset-angular/setup-jest';

Update tsconfig.spec.json:

json
{
  "compilerOptions": {
    "types": ["jest"]
  }
}

Running Tests

bash
# Run all tests
npx jest

# Run tests in watch mode (re-runs on file changes)
npx jest --watch

# Run a specific test file
npx jest src/utils/math.test.ts

# Run with coverage
npx jest --coverage

Your First Test

The AAA Pattern

Every good test follows the Arrange-Act-Assert pattern — the three phases of a unit test:

typescript
// math.ts
export function add(a: number, b: number): number {
  return a + b;
}
typescript
// math.test.ts
import { add } from './math';

describe('add', () => {
  it('should add two positive numbers correctly', () => {
    // Arrange — set up the inputs
    const a = 3;
    const b = 5;

    // Act — execute the function under test
    const result = add(a, b);

    // Assert — verify the output
    expect(result).toBe(8);
  });

  it('should handle negative numbers', () => {
    expect(add(-3, -5)).toBe(-8);
  });

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

Breaking this down:

  • describe groups related tests together under a label. You can nest describe blocks for sub-grouping (e.g., describe('add') → describe('with negative numbers')).
  • it (or test — they're identical) defines a single test case. The string should read like a sentence: "it should add two positive numbers correctly."
  • expect creates an assertion. You pass in the actual value and chain a matcher (like .toBe()) to declare what you expect.

The Test File Convention

Jest automatically finds test files matching these patterns:

PatternExample
*.test.ts / *.test.tsxmath.test.ts
*.spec.ts / *.spec.tsxmath.spec.ts
Files inside __tests__/__tests__/math.ts

Recommendation: Place test files next to the source file they test. This makes it trivial to find the tests for any given file and keeps related code co-located.


Matchers Deep Dive

Matchers are the assertion methods you chain after expect(). Jest ships with a rich set of matchers that cover virtually every testing scenario.

Equality Matchers

typescript
// toBe — strict equality (===), use for primitives
expect(2 + 2).toBe(4);
expect('hello').toBe('hello');

// toEqual — deep equality, use for objects and arrays
expect({ name: 'Alice', age: 30 }).toEqual({ name: 'Alice', age: 30 });
expect([1, 2, 3]).toEqual([1, 2, 3]);

// ⚠️ Common mistake: toBe fails for objects even with identical content
expect({ a: 1 }).toBe({ a: 1 }); // ❌ FAILS — different references
expect({ a: 1 }).toEqual({ a: 1 }); // ✅ PASSES — same structure

The critical distinction: toBe checks reference identity (===), while toEqual recursively checks every property. For objects and arrays, always use toEqual. For primitives (numbers, strings, booleans), either works, but toBe is conventional.

Truthiness Matchers

typescript
// Checking for null, undefined, and truthiness
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();
expect(1).toBeTruthy();
expect(0).toBeFalsy();

These matchers are invaluable when testing functions that return optional values or when you need to verify that a variable was properly initialized. toBeTruthy() matches any value that JavaScript treats as truthy in a boolean context, while toBeFalsy() matches false, 0, '', null, undefined, and NaN.

Number Matchers

typescript
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(5).toBeLessThanOrEqual(5);

// For floating-point numbers — NEVER use toBe
expect(0.1 + 0.2).toBe(0.3);          // ❌ FAILS (floating-point precision)
expect(0.1 + 0.2).toBeCloseTo(0.3);   // ✅ PASSES (within rounding tolerance)

Floating-point arithmetic is notoriously imprecise in all programming languages. toBeCloseTo accounts for this by checking if the values are within a small epsilon. Always use it when comparing decimal results.

String Matchers

typescript
expect('Hello, World!').toContain('World');
expect('Hello, World!').toMatch(/hello/i);  // regex
expect('Jest is awesome').toMatch('awesome');

// Negation — any matcher can be inverted with .not
expect('Hello').not.toContain('Goodbye');

Array and Iterable Matchers

typescript
const shoppingList = ['milk', 'eggs', 'bread', 'butter'];

expect(shoppingList).toContain('eggs');
expect(shoppingList).toHaveLength(4);
expect(shoppingList).toEqual(expect.arrayContaining(['eggs', 'bread']));

// Object matching within arrays
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
];
expect(users).toContainEqual({ id: 1, name: 'Alice' });

expect.arrayContaining is particularly useful when you want to assert that an array contains certain elements without caring about order or other elements. It pairs perfectly with API response testing.

Exception Matchers

typescript
function validateAge(age: number): void {
  if (age < 0) throw new Error('Age cannot be negative');
  if (age > 150) throw new RangeError('Age is unrealistic');
}

// Must wrap in a function to catch the throw
expect(() => validateAge(-1)).toThrow();
expect(() => validateAge(-1)).toThrow('Age cannot be negative');
expect(() => validateAge(-1)).toThrow(Error);
expect(() => validateAge(200)).toThrow(RangeError);

Important: When testing thrown errors, you must wrap the call in an arrow function. If you write expect(validateAge(-1)).toThrow(), the error fires before Jest can catch it, and your test crashes.


Mocking Fundamentals

Mocking is where unit testing gets powerful — and where most developers struggle. A mock replaces a real dependency with a controlled substitute, so you can test your code in isolation without worrying about databases, APIs, or file systems.

Why Mock?

Consider this function:

typescript
import { fetchUserFromAPI } from './api';

export async function getGreeting(userId: string): Promise<string> {
  const user = await fetchUserFromAPI(userId);
  return `Hello, ${user.name}!`;
}

To test getGreeting, you don't want to make a real API call. You want to mock fetchUserFromAPI so it returns a predictable response instantly.

jest.fn() — Creating Mock Functions

jest.fn() creates a mock function that tracks how it's been called:

typescript
const mockFn = jest.fn();

// Call the mock
mockFn('hello', 42);
mockFn('world');

// Assert how it was called
expect(mockFn).toHaveBeenCalled();               // Was it called at all?
expect(mockFn).toHaveBeenCalledTimes(2);          // How many times?
expect(mockFn).toHaveBeenCalledWith('hello', 42); // With these exact arguments?
expect(mockFn).toHaveBeenLastCalledWith('world'); // Last call arguments?

Mock Return Values

typescript
const mockFn = jest.fn();

// Return a specific value
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);

// Return different values on consecutive calls
mockFn
  .mockReturnValueOnce('first')
  .mockReturnValueOnce('second')
  .mockReturnValue('default');

expect(mockFn()).toBe('first');
expect(mockFn()).toBe('second');
expect(mockFn()).toBe('default');
expect(mockFn()).toBe('default'); // subsequent calls return default

// For async functions
const mockAsync = jest.fn().mockResolvedValue({ name: 'Alice' });
const result = await mockAsync();
expect(result).toEqual({ name: 'Alice' });

jest.mock() — Mocking Entire Modules

This is one of Jest's most powerful features. jest.mock() replaces an entire module with mock implementations:

typescript
// user-service.ts
import { fetchUser } from './api';

export async function getUserDisplayName(id: string): Promise<string> {
  const user = await fetchUser(id);
  return `${user.firstName} ${user.lastName}`;
}
typescript
// user-service.test.ts
import { getUserDisplayName } from './user-service';
import { fetchUser } from './api';

// Mock the entire './api' module
jest.mock('./api');

// TypeScript: cast to mock type
const mockFetchUser = fetchUser as jest.MockedFunction<typeof fetchUser>;

describe('getUserDisplayName', () => {
  it('should return the full display name', async () => {
    // Arrange — configure the mock's return value
    mockFetchUser.mockResolvedValue({
      id: '1',
      firstName: 'Alice',
      lastName: 'Johnson',
    });

    // Act
    const name = await getUserDisplayName('1');

    // Assert
    expect(name).toBe('Alice Johnson');
    expect(mockFetchUser).toHaveBeenCalledWith('1');
  });
});

When you call jest.mock('./api'), Jest automatically replaces every export from ./api with jest.fn(). You then configure each mock individually in your tests.

jest.spyOn() — Spying on Existing Methods

Sometimes you don't want to replace a function entirely — you just want to observe it and optionally override its behavior:

typescript
const calculator = {
  add(a: number, b: number) {
    return a + b;
  },
  multiply(a: number, b: number) {
    return a * b;
  },
};

// Spy on the 'add' method
const spy = jest.spyOn(calculator, 'add');

calculator.add(2, 3);

expect(spy).toHaveBeenCalledWith(2, 3);
expect(spy).toHaveReturnedWith(5);

// Override the implementation temporarily
spy.mockReturnValue(999);
expect(calculator.add(2, 3)).toBe(999);

// Restore the original
spy.mockRestore();
expect(calculator.add(2, 3)).toBe(5);

When to use spyOn vs jest.mock():

ScenarioUse
Replace an entire modulejest.mock()
Override one method on an objectjest.spyOn()
Track calls to a real methodjest.spyOn() without override
Test a function that calls another in the same filejest.spyOn() on the module

Clearing, Resetting, and Restoring Mocks

This is a source of confusion. Here's the definitive guide:

typescript
const mockFn = jest.fn().mockReturnValue(42);
mockFn('test');

// mockClear — clears call history only (keeps implementation)
mockFn.mockClear();
expect(mockFn).not.toHaveBeenCalled(); // ✅ history cleared
expect(mockFn()).toBe(42);              // ✅ still returns 42

// mockReset — clears history AND implementation
mockFn.mockReset();
expect(mockFn()).toBeUndefined();       // ✅ returns undefined

// mockRestore — restores the original (only works with spyOn)
const spy = jest.spyOn(console, 'log');
spy.mockRestore(); // console.log is back to normal
MethodClears HistoryClears ImplementationRestores Original
mockClear()✅❌❌
mockReset()✅✅❌
mockRestore()✅✅✅

Best practice: Use jest.clearAllMocks() in a beforeEach block to ensure every test starts with a clean call history:

typescript
beforeEach(() => {
  jest.clearAllMocks();
});

Mocking Modules and API Calls

Mocking Axios/Fetch

API calls are the most common thing you'll mock. Here's how to handle both axios and fetch:

typescript
// api.ts
import axios from 'axios';

export async function getUsers(): Promise<User[]> {
  const response = await axios.get('/api/users');
  return response.data;
}
typescript
// api.test.ts
import axios from 'axios';
import { getUsers } from './api';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('getUsers', () => {
  it('should fetch users from the API', async () => {
    const mockUsers = [{ id: 1, name: 'Alice' }];
    mockedAxios.get.mockResolvedValue({ data: mockUsers });

    const users = await getUsers();

    expect(users).toEqual(mockUsers);
    expect(mockedAxios.get).toHaveBeenCalledWith('/api/users');
  });

  it('should handle API errors gracefully', async () => {
    mockedAxios.get.mockRejectedValue(new Error('Network Error'));

    await expect(getUsers()).rejects.toThrow('Network Error');
  });
});

Mocking fetch (No Library)

typescript
// Without a library, mock the global fetch
global.fetch = jest.fn();

describe('fetchData', () => {
  it('should parse JSON response', async () => {
    (global.fetch as jest.Mock).mockResolvedValue({
      ok: true,
      json: async () => ({ id: 1, name: 'Alice' }),
    });

    const response = await fetch('/api/users/1');
    const data = await response.json();

    expect(data).toEqual({ id: 1, name: 'Alice' });
  });
});

Partial Module Mocks

Sometimes you want to mock one export from a module while keeping the rest real:

typescript
// Only mock 'fetchUser' but keep 'formatUser' real
jest.mock('./user-utils', () => ({
  ...jest.requireActual('./user-utils'),
  fetchUser: jest.fn(),
}));

jest.requireActual() imports the real module, and you override only the specific exports you need to mock. This is essential when a module has both utility functions and API functions — you want real utilities but mocked API calls.


Testing Async Code

Modern applications are heavily asynchronous. Jest handles every async pattern:

Async/Await (Recommended)

typescript
// The cleanest approach for most async tests
it('should fetch user data', async () => {
  const user = await fetchUser('123');
  expect(user.name).toBe('Alice');
});

// Testing rejections
it('should throw on invalid ID', async () => {
  await expect(fetchUser('')).rejects.toThrow('ID required');
});

Promises

typescript
it('should resolve with user data', () => {
  return expect(fetchUser('123')).resolves.toEqual({
    id: '123',
    name: 'Alice',
  });
});

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

Note: When using .resolves or .rejects without async/await, you must return the promise. If you forget, the test will pass before the assertion runs.

Timers

Testing setTimeout, setInterval, and Date.now():

typescript
// debounce.ts
export function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): T {
  let timer: ReturnType<typeof setTimeout>;
  return ((...args: any[]) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  }) as T;
}
typescript
// debounce.test.ts
import { debounce } from './debounce';

describe('debounce', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('should call the function after the delay', () => {
    const mockFn = jest.fn();
    const debounced = debounce(mockFn, 500);

    debounced('hello');

    // Function hasn't been called yet
    expect(mockFn).not.toHaveBeenCalled();

    // Fast-forward 500ms
    jest.advanceTimersByTime(500);

    // Now it should have been called
    expect(mockFn).toHaveBeenCalledWith('hello');
    expect(mockFn).toHaveBeenCalledTimes(1);
  });

  it('should reset the timer on subsequent calls', () => {
    const mockFn = jest.fn();
    const debounced = debounce(mockFn, 500);

    debounced('first');
    jest.advanceTimersByTime(300);

    debounced('second'); // resets the timer
    jest.advanceTimersByTime(300);

    expect(mockFn).not.toHaveBeenCalled(); // only 300ms since reset

    jest.advanceTimersByTime(200);
    expect(mockFn).toHaveBeenCalledWith('second');
    expect(mockFn).toHaveBeenCalledTimes(1);
  });
});

jest.useFakeTimers() replaces setTimeout, setInterval, Date.now(), and other time-related functions with controllable versions. jest.advanceTimersByTime(ms) fast-forwards the clock by the specified milliseconds, causing any pending timers to fire. This eliminates flaky tests caused by real timing delays.


Setup and Teardown

When multiple tests share setup logic, use lifecycle hooks to avoid duplication:

typescript
describe('Database operations', () => {
  let db: Database;

  // Runs ONCE before all tests in this describe block
  beforeAll(async () => {
    db = await Database.connect('test-db');
  });

  // Runs ONCE after all tests in this describe block
  afterAll(async () => {
    await db.disconnect();
  });

  // Runs before EACH individual test
  beforeEach(async () => {
    await db.clear();
    await db.seed(testData);
  });

  // Runs after EACH individual test
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should insert a record', async () => {
    await db.insert({ name: 'Alice' });
    const users = await db.findAll();
    expect(users).toHaveLength(testData.length + 1);
  });

  it('should delete a record', async () => {
    await db.delete(1);
    const users = await db.findAll();
    expect(users).toHaveLength(testData.length - 1);
  });
});

Scoping with Nested describe

Lifecycle hooks are scoped to their describe block. This lets you create specialized setup for groups of related tests:

typescript
describe('UserService', () => {
  beforeEach(() => {
    // Shared setup for ALL UserService tests
    jest.clearAllMocks();
  });

  describe('when user exists', () => {
    beforeEach(() => {
      // Setup specific to "user exists" tests
      mockFetchUser.mockResolvedValue({ id: '1', name: 'Alice' });
    });

    it('should return the user', async () => {
      const user = await userService.getUser('1');
      expect(user.name).toBe('Alice');
    });

    it('should return cached user on second call', async () => {
      await userService.getUser('1');
      await userService.getUser('1');
      expect(mockFetchUser).toHaveBeenCalledTimes(1);
    });
  });

  describe('when user does not exist', () => {
    beforeEach(() => {
      mockFetchUser.mockResolvedValue(null);
    });

    it('should throw UserNotFoundError', async () => {
      await expect(userService.getUser('999')).rejects.toThrow('not found');
    });
  });
});

This nested structure makes your test output highly readable:

UserService
  when user exists
    ✓ should return the user
    ✓ should return cached user on second call
  when user does not exist
    ✓ should throw UserNotFoundError

Code Coverage

Jest has built-in code coverage powered by Istanbul. Run with --coverage:

bash
npx jest --coverage

Understanding the Coverage Report

----------------------|---------|----------|---------|---------|
File                  | % Stmts | % Branch | % Funcs | % Lines |
----------------------|---------|----------|---------|---------|
All files             |   85.71 |    66.67 |     100 |   83.33 |
 math.ts              |     100 |      100 |     100 |     100 |
 user-service.ts      |   71.43 |    33.33 |     100 |   66.67 |
----------------------|---------|----------|---------|---------|
MetricWhat It Measures
StatementsPercentage of code statements executed during tests
BranchesPercentage of conditional branches (if/else, ternary, switch cases) covered
FunctionsPercentage of functions that were called at least once
LinesPercentage of executable lines that ran

Setting Coverage Thresholds

Add to jest.config.ts to enforce minimum coverage:

typescript
export default {
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

If coverage drops below these thresholds, jest --coverage will fail. This is valuable in CI/CD pipelines to prevent coverage regression.

What Should You NOT Cover?

  • Third-party libraries — You don't test React, Angular, or lodash
  • Type definitions — TypeScript interfaces have no runtime code
  • Configuration files — jest.config.ts, tsconfig.json, etc.
  • Index files — Barrel files that just re-export

Add these to coveragePathIgnorePatterns in your jest config.


Snapshot Testing

Snapshots capture the output of a function or component and save it to a file. On subsequent runs, Jest compares the new output to the saved snapshot.

typescript
// format-user.ts
export function formatUserCard(user: User): string {
  return `
    Name: ${user.name}
    Email: ${user.email}
    Role: ${user.role}
    Joined: ${user.joinDate}
  `.trim();
}
typescript
// format-user.test.ts
import { formatUserCard } from './format-user';

it('should match the expected card format', () => {
  const user = {
    name: 'Alice Johnson',
    email: 'alice@example.com',
    role: 'Admin',
    joinDate: '2024-01-15',
  };

  expect(formatUserCard(user)).toMatchSnapshot();
});

First run: Jest creates a .snap file with the output. Subsequent runs: Jest compares the current output against the snapshot. If they differ, the test fails.

Updating Snapshots

When intentional changes cause snapshot failures:

bash
npx jest --updateSnapshot
# or shorthand
npx jest -u

Inline Snapshots

For small outputs, inline snapshots are cleaner:

typescript
it('should format the greeting', () => {
  expect(greet('Alice')).toMatchInlineSnapshot(`"Hello, Alice! Welcome back."`);
});

The snapshot is stored right in the test file, making it easy to review.

When to Use and NOT Use Snapshots

✅ Good Use Cases❌ Bad Use Cases
Serialized data formatsDynamic content (timestamps, random IDs)
Error message formattingLarge component trees
Configuration objectsFrequently changing UIs
API response shapesAnything where a specific assertion is clearer

The golden rule: If a specific assertion (toBe, toEqual) can express your intent clearly, prefer it over a snapshot. Snapshots are best as a safety net for output format, not as a substitute for meaningful assertions.


Testing React Components

React Testing Library (RTL) is the standard for testing React components. Its philosophy: test what the user sees and does, not implementation details.

Rendering and Querying

tsx
// Greeting.tsx
interface GreetingProps {
  name: string;
  isLoggedIn: boolean;
}

export function Greeting({ name, isLoggedIn }: GreetingProps) {
  if (!isLoggedIn) {
    return <p>Please log in to continue.</p>;
  }
  return (
    <div>
      <h1>Welcome back, {name}!</h1>
      <p>You have 3 new notifications.</p>
    </div>
  );
}
tsx
// Greeting.test.tsx
import { render, screen } from '@testing-library/react';
import { Greeting } from './Greeting';

describe('Greeting', () => {
  it('should show login prompt when not logged in', () => {
    render(<Greeting name="Alice" isLoggedIn={false} />);

    expect(screen.getByText('Please log in to continue.')).toBeInTheDocument();
    expect(screen.queryByText('Welcome back')).not.toBeInTheDocument();
  });

  it('should show welcome message when logged in', () => {
    render(<Greeting name="Alice" isLoggedIn={true} />);

    expect(screen.getByText('Welcome back, Alice!')).toBeInTheDocument();
    expect(screen.getByText('You have 3 new notifications.')).toBeInTheDocument();
  });
});

Query Priority

RTL provides multiple queries. Use them in this priority order (most accessible first):

PriorityQueryUse When
1stgetByRoleElement has an ARIA role (button, heading, textbox)
2ndgetByLabelTextForm elements with associated labels
3rdgetByPlaceholderTextInput with placeholder
4thgetByTextNon-interactive elements with visible text
5thgetByTestIdLast resort — no accessible way to query

Testing User Interactions

tsx
// Counter.tsx
import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <button onClick={() => setCount(c => c - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}
tsx
// Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter', () => {
  it('should start at zero', () => {
    render(<Counter />);
    expect(screen.getByText('Count: 0')).toBeInTheDocument();
  });

  it('should increment when clicking the increment button', async () => {
    const user = userEvent.setup();
    render(<Counter />);

    await user.click(screen.getByRole('button', { name: 'Increment' }));
    expect(screen.getByText('Count: 1')).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: 'Increment' }));
    expect(screen.getByText('Count: 2')).toBeInTheDocument();
  });

  it('should reset to zero', async () => {
    const user = userEvent.setup();
    render(<Counter />);

    await user.click(screen.getByRole('button', { name: 'Increment' }));
    await user.click(screen.getByRole('button', { name: 'Increment' }));
    await user.click(screen.getByRole('button', { name: 'Reset' }));

    expect(screen.getByText('Count: 0')).toBeInTheDocument();
  });
});

Testing Forms with User Events

tsx
// LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

describe('LoginForm', () => {
  it('should submit with email and password', async () => {
    const user = userEvent.setup();
    const mockSubmit = jest.fn();
    render(<LoginForm onSubmit={mockSubmit} />);

    await user.type(screen.getByLabelText('Email'), 'alice@example.com');
    await user.type(screen.getByLabelText('Password'), 'secure123');
    await user.click(screen.getByRole('button', { name: 'Sign In' }));

    expect(mockSubmit).toHaveBeenCalledWith({
      email: 'alice@example.com',
      password: 'secure123',
    });
  });

  it('should show validation error for empty email', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={jest.fn()} />);

    await user.click(screen.getByRole('button', { name: 'Sign In' }));

    expect(screen.getByText('Email is required')).toBeInTheDocument();
  });
});

Testing Angular Components with Jest

While Angular traditionally uses Karma, Jest works perfectly with Angular via jest-preset-angular. The key difference is using TestBed for dependency injection:

Basic Component Test

typescript
// greeting.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-greeting',
  template: `
    <div *ngIf="isLoggedIn; else loginPrompt">
      <h1>Welcome back, {{ name }}!</h1>
      <p>You have {{ notificationCount }} new notifications.</p>
    </div>
    <ng-template #loginPrompt>
      <p>Please log in to continue.</p>
    </ng-template>
  `,
})
export class GreetingComponent {
  @Input() name = '';
  @Input() isLoggedIn = false;
  @Input() notificationCount = 0;
}
typescript
// greeting.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GreetingComponent } from './greeting.component';

describe('GreetingComponent', () => {
  let component: GreetingComponent;
  let fixture: ComponentFixture<GreetingComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [GreetingComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(GreetingComponent);
    component = fixture.componentInstance;
  });

  it('should show login prompt when not logged in', () => {
    component.isLoggedIn = false;
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;
    expect(element.textContent).toContain('Please log in to continue.');
    expect(element.querySelector('h1')).toBeNull();
  });

  it('should show welcome message when logged in', () => {
    component.name = 'Alice';
    component.isLoggedIn = true;
    component.notificationCount = 3;
    fixture.detectChanges();

    const element = fixture.nativeElement as HTMLElement;
    expect(element.querySelector('h1')?.textContent).toContain('Welcome back, Alice!');
    expect(element.textContent).toContain('3 new notifications');
  });
});

Testing a Component with a Service

typescript
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  getUser(id: string): Observable<User> {
    return this.http.get<User>(`/api/users/${id}`);
  }
}
typescript
// user-profile.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { UserProfileComponent } from './user-profile.component';
import { UserService } from './user.service';

describe('UserProfileComponent', () => {
  let component: UserProfileComponent;
  let fixture: ComponentFixture<UserProfileComponent>;
  let mockUserService: jest.Mocked<UserService>;

  beforeEach(async () => {
    // Create a mock UserService with jest.fn() methods
    mockUserService = {
      getUser: jest.fn(),
    } as any;

    await TestBed.configureTestingModule({
      declarations: [UserProfileComponent],
      providers: [
        { provide: UserService, useValue: mockUserService },
      ],
    }).compileComponents();

    fixture = TestBed.createComponent(UserProfileComponent);
    component = fixture.componentInstance;
  });

  it('should display user name after loading', () => {
    mockUserService.getUser.mockReturnValue(
      of({ id: '1', name: 'Alice', email: 'alice@example.com' })
    );

    fixture.detectChanges(); // triggers ngOnInit

    const element = fixture.nativeElement as HTMLElement;
    expect(element.textContent).toContain('Alice');
  });
});

Key difference from React: Angular uses dependency injection through TestBed.configureTestingModule. You provide mock services using the providers array with { provide: RealService, useValue: mockService }. This is Angular's native way of wiring dependencies, making test setup declarative and predictable.


Common Mistakes to Avoid

Mistake 1: Testing Implementation Details

typescript
// ❌ BAD — tests HOW it works (implementation detail)
it('should call setState with the new count', () => {
  const setState = jest.spyOn(React, 'useState');
  // ... testing internal state management
});

// ✅ GOOD — tests WHAT it does (behavior)
it('should display the incremented count after clicking', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  await user.click(screen.getByRole('button', { name: 'Increment' }));
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

Testing implementation details (like which internal methods are called or what the internal state looks like) creates brittle tests that break when you refactor, even if the behavior stays identical. Test the observable output — what the user sees and what external APIs receive.

Mistake 2: Not Testing Edge Cases

typescript
// ❌ Only testing the happy path
it('should divide two numbers', () => {
  expect(divide(10, 2)).toBe(5);
});

// ✅ Also test edge cases
it('should throw when dividing by zero', () => {
  expect(() => divide(10, 0)).toThrow('Division by zero');
});

it('should handle negative numbers', () => {
  expect(divide(-10, 2)).toBe(-5);
});

it('should return 0 when numerator is 0', () => {
  expect(divide(0, 5)).toBe(0);
});

Mistake 3: Over-Mocking

typescript
// ❌ Too many mocks — you're no longer testing real behavior
jest.mock('./utils');
jest.mock('./validators');
jest.mock('./formatters');
jest.mock('./logger');

it('should process the order', () => {
  // At this point, everything is mocked.
  // What are you actually testing?
});

If you find yourself mocking everything, your function might have too many dependencies. This is a design smell — consider refactoring to reduce coupling. Mock only external I/O boundaries (APIs, databases, file system) while keeping business logic real.

Mistake 4: Forgetting detectChanges() in Angular

typescript
// ❌ Angular: forgot to trigger change detection
it('should show the user name', () => {
  component.name = 'Alice';
  // Missing: fixture.detectChanges();
  expect(fixture.nativeElement.textContent).toContain('Alice'); // FAILS
});

// ✅ Always call detectChanges after setting inputs
it('should show the user name', () => {
  component.name = 'Alice';
  fixture.detectChanges();
  expect(fixture.nativeElement.textContent).toContain('Alice');
});

Angular's change detection doesn't run automatically in tests. You must manually call fixture.detectChanges() after modifying inputs or triggering events. This is intentional — it gives you full control over when the DOM updates, which is essential for testing async flows.


Best Practices Summary

PracticeWhy
Follow AAA (Arrange-Act-Assert)Makes tests readable and predictable
Test behavior, not implementationTests survive refactoring
Mock only I/O boundariesKeeps business logic real
Use clearAllMocks in beforeEachPrevents test pollution
Write descriptive test namesTests serve as documentation
Test edge casesCatches bugs before users do
Use coverage thresholdsPrevents regression
Prefer userEvent over fireEventSimulates real user behavior more accurately

Interview Preparation

Common Interview Questions

Q: What is the difference between toBe and toEqual?

toBe uses strict equality (===), meaning it checks reference identity. Two different objects with identical properties will fail toBe. toEqual performs a deep recursive comparison of all properties, so it passes as long as the structure and values match. Use toBe for primitives (numbers, strings, booleans) and toEqual for objects and arrays.

Q: Explain jest.fn(), jest.mock(), and jest.spyOn() — when do you use each?

jest.fn() creates a standalone mock function from scratch — use it when you need a mock callback or dependency. jest.mock() replaces an entire module's exports with mocks — use it when your function imports from another module and you want to control those imports. jest.spyOn() wraps an existing method on an object, tracking calls while optionally overriding behavior — use it when you want to observe calls to a real method or temporarily override one method without replacing the whole module.

Q: How do you test asynchronous code in Jest?

Jest supports multiple patterns: async/await (preferred — cleanest syntax), returning a promise from the test, using .resolves/.rejects matchers, and using the done callback (legacy). For timer-based async code, use jest.useFakeTimers() with jest.advanceTimersByTime() to avoid real waiting. Always ensure async tests are properly awaited — unawaited assertions silently pass.

Q: What's the difference between mockClear, mockReset, and mockRestore?

mockClear clears the call history (.mock.calls, .mock.results) but keeps the mock implementation. mockReset clears both history and implementation, making the mock return undefined. mockRestore does everything mockReset does AND restores the original implementation — but only works with jest.spyOn(). Best practice is to call jest.clearAllMocks() in beforeEach to ensure clean state between tests.

Q: What is snapshot testing and when should you use it?

Snapshot testing captures the serialized output of a value and saves it to a .snap file. On subsequent test runs, Jest compares the current output against the stored snapshot. Use snapshots for stable output formats (error messages, serialized data, configuration objects). Avoid them for frequently changing content, dynamic values (timestamps, random IDs), or large component trees. If a specific assertion like toBe or toEqual can express your intent clearly, prefer that over a snapshot.

Q: How does React Testing Library differ from Enzyme?

Enzyme tests components from the inside out — it gives access to internal state, props, and lifecycle methods. React Testing Library tests from the outside in — it only interacts with what a real user can see and do (DOM elements, text content, ARIA roles). RTL's approach produces tests that are more resilient to refactoring because they don't depend on implementation details. The React team officially recommends Testing Library over Enzyme, and Enzyme hasn't been updated for React 18+.


Conclusion

Jest isn't just a testing framework — it's a development accelerator. When you have a comprehensive test suite, you can:

  1. Refactor fearlessly — tests catch regressions instantly
  2. Ship faster — automated tests replace manual QA for unit-level logic
  3. onboard faster — tests serve as living documentation of how your code works
  4. Debug faster — a failing test tells you exactly what broke and where

The key principles to remember:

  • Follow AAA (Arrange-Act-Assert) for every test
  • Mock only at I/O boundaries — keep business logic real
  • Test behavior, not implementation — your tests should survive refactoring
  • Cover edge cases — the happy path is the easy part

Whether you're testing a pure utility function, a React component, or an Angular service, the patterns are the same. Master them once, apply them everywhere.

💡 tip

[!TIP] Start every feature by writing the test first (Test-Driven Development). Even if you don't follow strict TDD, writing the test first forces you to think about the API, edge cases, and expected behavior before you write a single line of implementation code.

🧠 Test Your Knowledge

Now that you've learned the concepts, let's see if you can apply them! Take this quick quiz to test your understanding.

PreviousWeb Accessibility 101: A Developer's Guide to WCAG & ARIANextJavaScript Array Methods Mastery: Complete Guide from map() to reduce()

Written by

Shaik Munsif

Read more articles

Found this helpful? Share it with your network!

On this page

0/59
Question 1 of 10Easy
Score: 0/0

What is the difference between toBe and toEqual in Jest?