Unit 6.4 · Tutorial 1

Unit & Integration Testing

Chapter 20 · Advanced Topics
~3 hours Intermediate Jest · Vitest · RTL · Mocking

Overview

Testing is the foundation of reliable software. This tutorial covers the testing pyramid, writing unit tests with Jest and Vitest, testing React components with React Testing Library, and mocking dependencies with spies and mocks. You'll learn to build a comprehensive test suite that catches bugs before they reach production.

Why this matters: Tests give you confidence to refactor, catch regressions, and ship features faster. A good test suite is the safety net of any professional codebase.

1. The Testing Pyramid

The testing pyramid is a strategy for balancing test types:

  • Unit Tests (Base): Fast, isolated tests of individual functions or components. Many tests.
  • Integration Tests (Middle): Tests that verify interactions between components or services.
  • E2E Tests (Top): Full application tests that simulate real user interactions. Few tests.
// Testing Pyramid Visual ┌──────────────────────┐ │ E2E Tests │ <-- Few, slow, high confidence │ (Cypress/Playwright)│ ├──────────────────────┤ │ Integration Tests │ <-- Some, medium speed │ (Jest + Supertest) │ ├──────────────────────┤ │ Unit Tests │ <-- Many, fast, low cost │ (Jest/Vitest) │ └──────────────────────┘
Best practice: Write more unit tests than integration tests, and more integration tests than E2E tests. This gives you fast feedback and high confidence.

2. Jest Fundamentals

Jest is the most popular testing framework for JavaScript/TypeScript.

# Installation npm install --save-dev jest @types/jest npm install --save-dev ts-jest # for TypeScript
// math.test.ts import { describe, expect, it } from '@jest/globals'; function add(a: number, b: number): number { return a + b; } function multiply(a: number, b: number): number { return a * b; } describe('Math operations', () => { it('adds two numbers correctly', () => { expect(add(2, 3)).toBe(5); expect(add(-1, 1)).toBe(0); expect(add(0, 0)).toBe(0); }); it('multiplies two numbers correctly', () => { expect(multiply(2, 3)).toBe(6); expect(multiply(-1, 5)).toBe(-5); }); });

Common Jest Matchers

// Basic matchers expect(value).toBe(expected) // strict equality (===) expect(value).toEqual(expected) // deep equality expect(value).toBeTruthy() // truthy expect(value).toBeFalsy() // falsy expect(value).toBeNull() // null expect(value).toBeUndefined() // undefined // Number matchers expect(value).toBeGreaterThan(3) expect(value).toBeLessThan(10) expect(value).toBeCloseTo(0.3, 2) // floating point // Array / object matchers expect(array).toContain('item') expect(array).toHaveLength(3) expect(object).toHaveProperty('name', 'Alice') // Error matchers expect(() => { throw new Error('fail') }).toThrow('fail')

3. Vitest & Modern Testing

Vitest is a modern testing framework built on Vite. It's faster than Jest for Vite projects and has a compatible API.

# Installation npm install --save-dev vitest @vitest/ui
// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], test: { globals: true, environment: 'jsdom', setupFiles: './src/test/setup.ts', }, });
// component.test.tsx (Vitest) import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import { Button } from './Button'; describe('Button component', () => { it('renders with the provided label', () => { render(

Vitest vs Jest: Vitest is faster (especially with Vite), has built‑in ESM support, and a Jest‑compatible API. Jest is more mature with a larger ecosystem.

4. React Testing Library

React Testing Library (RTL) is the official testing library for React. It encourages testing components as users would interact with them.

# Installation npm install --save-dev @testing-library/react @testing-library/jest-dom npm install --save-dev @testing-library/user-event
// Counter.tsx import { useState } from 'react'; export function Counter() { const [count, setCount] = useState(0); return (

Count: {count}

); }
// Counter.test.tsx import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Counter } from './Counter'; describe('Counter', () => { it('renders with initial count of 0', () => { render( ); expect(screen.getByText('Count: 0')).toBeTruthy(); }); it('increments the count when Increment button is clicked', async () => { const user = userEvent.setup(); render( ); const incrementBtn = screen.getByText('Increment'); await user.click(incrementBtn); await user.click(incrementBtn); expect(screen.getByText('Count: 2')).toBeTruthy(); }); it('decrements the count when Decrement button is clicked', async () => { const user = userEvent.setup(); render( ); const decrementBtn = screen.getByText('Decrement'); await user.click(decrementBtn); expect(screen.getByText('Count: -1')).toBeTruthy(); }); });
RTL Philosophy: Test what users see and do, not implementation details. Use getByText, getByRole, getByLabelText to find elements as users would.

5. Mocking & Spies

Mocks replace real implementations with controlled versions for testing.

Jest Mock Functions

// api.ts export async function fetchUser(id: number) { const res = await fetch(`/api/users/${id}`); return res.json(); } // api.test.ts import { fetchUser } from './api'; // Mock the global fetch global.fetch = jest.fn(); describe('fetchUser', () => { it('fetches a user by ID', async () => { const mockUser = { id: 1, name: 'Alice' }; (global.fetch as jest.Mock).mockResolvedValue({ json: async () => mockUser, }); const result = await fetchUser(1); expect(result).toEqual(mockUser); expect(global.fetch).toHaveBeenCalledWith('/api/users/1'); }); });

Spies

// Logger service export const logger = { info: (msg: string) => console.log(msg), error: (msg: string) => console.error(msg), }; // Logger.test.ts import { logger } from './logger'; describe('logger', () => { it('calls console.log with the message', () => { const spy = jest.spyOn(console, 'log'); logger.info('test message'); expect(spy).toHaveBeenCalledWith('test message'); spy.mockRestore(); }); });

Mocking modules

// Mock an entire module jest.mock('axios', () => ({ get: jest.fn(() => Promise.resolve({ data: [] })), post: jest.fn(() => Promise.resolve({ data: {} })), })); // Mock a specific function jest.mock('./utils', () => ({ ...jest.requireActual('./utils'), formatDate: jest.fn(() => '2026-09-01'), }));

Quiz

Question 1

In the testing pyramid, which type of test is the most abundant?

  • E2E Tests
  • Integration Tests
  • Unit Tests
  • Smoke Tests
Show answer
C. Unit Tests. They are the fastest and cheapest to write and run.

Question 2

Which function in React Testing Library finds an element by its text content?

  • getByTestId
  • getByText
  • getByRole
  • getByLabel
Show answer
B. getByText.

Question 3

What is the purpose of jest.fn()?

  • To run a test function
  • To create a mock function for testing
  • To skip a test
  • To configure Jest
Show answer
B. To create a mock function for testing.

Exercises

Exercise 1

Write Jest tests for a calculateTotal function that takes an array of prices and returns the sum. Include edge cases (empty array, negative numbers).

Sample answer
function calculateTotal(prices: number[]): number { return prices.reduce((sum, price) => sum + price, 0); } describe('calculateTotal', () => { it('returns sum of positive prices', () => { expect(calculateTotal([10, 20, 30])).toBe(60); }); it('returns correct sum with negative prices', () => { expect(calculateTotal([10, -5, 20])).toBe(25); }); it('returns 0 for empty array', () => { expect(calculateTotal([])).toBe(0); }); });

Exercise 2

Write a React Testing Library test for a TodoList component that renders a list of todos and allows adding new todos.

Sample answer
import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TodoList } from './TodoList'; describe('TodoList', () => { it('renders initial todos', () => { render( ); expect(screen.getByText('Buy milk')).toBeTruthy(); expect(screen.getByText('Walk dog')).toBeTruthy(); }); it('adds a new todo when Add button is clicked', async () => { const user = userEvent.setup(); render( ); const input = screen.getByPlaceholderText('Add a todo'); await user.type(input, 'Learn testing'); const addBtn = screen.getByText('Add'); await user.click(addBtn); expect(screen.getByText('Learn testing')).toBeTruthy(); }); });

Homework

Homework 1

Write unit tests for the backend API endpoints of your capstone project using Jest and Supertest. Include tests for GET, POST, PUT, and DELETE operations with mocked database calls.

Sample outline
  • Setup: Jest with Supertest for Express API testing.
  • Mock database: Mock Prisma/TypeORM methods.
  • Tests: GET /api/products (success, not found), POST /api/products (success, validation error), PUT /api/products/:id, DELETE /api/products/:id.
  • Coverage: Aim for >80% coverage on critical paths.

Mini‑Project

Comprehensive Test Suite

Build a comprehensive test suite for a full‑stack task management application:

  • Backend: Jest + Supertest for API endpoints
  • Frontend: Vitest + React Testing Library for components
  • At least 15 unit tests
  • At least 5 integration tests
  • Mock external dependencies (database, API calls)
Sample outline
  • Backend tests: Task CRUD, validation, error handling.
  • Frontend tests: TaskList component, TaskForm component, API hooks.
  • Integration tests: Full flow from UI to API (with mocked API).
  • Coverage: Run jest --coverage and aim for 80%+.

Tutorial Summary

You learned the fundamentals of unit and integration testing: the testing pyramid, Jest and Vitest frameworks, React Testing Library for component testing, and mocking/spying techniques. These skills are essential for building reliable, maintainable applications with high confidence.

Key takeaway: Testing is not a chore — it's an investment in code quality and developer productivity. A good test suite catches bugs early and gives you the confidence to refactor and ship features quickly.