Part 1.6 — Testing React Components with Jest (Preview of Part 5)
Testing isn’t about catching bugs you know exist — it’s about preventing bugs you never anticipated.
As your React app grows, tests act as guardrails that keep your UI stable, even as you refactor or expand features.
This post introduces Jest and React Testing Library (RTL) — the modern standard for testing React components. You’ll write clean, type-safe tests that verify your UI behaves the way users expect.
1. Why Test React Components?
A few reasons why full-stack engineers invest heavily in frontend tests:
Prevent regressions as features grow
Validate key user flows before integrating with the backend
Catch type, logic, and state bugs early
Enable safe refactoring
Increase team confidence
Testing feels slow at first — but it saves hours when your app hits real users.
2. Install Testing Dependencies
Inside your React workspace (apps/web):
pnpm add -D jest @types/jest ts-jest @testing-library/react @testing-library/jest-domThen initialize Jest:
pnpm ts-jest config:initThis generates a jest.config.js.
Update it to support JSX and modern React testing:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom'],
};Now you're ready to test components in a DOM-like environment.
3. Create a Simple Component to Test
Let’s test a small, deterministic component so the concepts are clear.
// src/components/Counter.tsx
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<p>Count: {count}</p>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
</section>
);
}A perfect candidate for a basic unit test.
4. Writing Your First Test
Create a test file:
src/components/Counter.test.tsxAdd:
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';
test('increments the counter when clicking the button', () => {
render(<Counter />);
expect(screen.getByText(/Count: 0/)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText(/Count: 1/)).toBeInTheDocument();
});What’s happening here?
render mounts the component into a virtual DOM
screen.getByText mirrors how real users search for information
fireEvent.click simulates the button interaction
The final assertion verifies the count incremented
This pattern recreates realistic user behavior — the core philosophy of RTL.
5. Testing Props and Variants
Let’s test a reusable component from earlier — a Button variant.
// src/components/Button.tsx
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary';
onClick: () => void;
}
export function Button({ label, variant = 'primary', onClick }: ButtonProps) {
return (
<button className={`btn-${variant}`} onClick={onClick}>
{label}
</button>
);
}Test file:
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
test('renders the button label', () => {
render(<Button label="Save" onClick={() => {}} />);
expect(screen.getByText('Save')).toBeInTheDocument();
});
test('applies the correct variant class', () => {
render(<Button label="Cancel" variant="secondary" onClick={() => {}} />);
const button = screen.getByRole('button', { name: /cancel/i });
expect(button).toHaveClass('btn-secondary');
});Small, isolated tests like these are easy to maintain and reason about.
6. Testing Form Components
Forms introduce events, controlled components, and validation logic.
Example: Test the AddTodoForm from the previous post.
// src/components/AddTodoForm.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { AddTodoForm } from './AddTodoForm';
test('submits a todo and clears the input', () => {
const mockAdd = vi.fn(); // if using Vitest; for Jest use jest.fn()
render(<AddTodoForm onAdd={mockAdd} />);
const input = screen.getByPlaceholderText(/add a new todo/i);
const button = screen.getByRole('button', { name: /add/i });
fireEvent.change(input, { target: { value: 'Buy milk' } });
fireEvent.click(button);
expect(mockAdd).toHaveBeenCalledWith('Buy milk');
expect(input).toHaveValue('');
});This test confirms:
Controlled input works
Submit logic fires
Input resets cleanly
You’re testing behavior — not implementation details.
7. Snapshot Testing (Use Sparingly)
You can create snapshots to catch unexpected UI changes:
import { render } from '@testing-library/react';
import { TodoItem } from './TodoItem';
test('renders todo item snapshot', () => {
const todo = { id: 1, text: 'Learn NestJS', completed: false };
const { container } = render(
<TodoItem todo={todo} onToggle={() => {}} onDelete={() => {}} />
);
expect(container).toMatchSnapshot();
});Snapshots can become noisy — use them for stable components only.
8. Running Your Tests
Add a script to package.json:
"scripts": {
"test": "jest"
}Run:
pnpm testIf everything is green, you’ve built your first test suite.
9. Why This Matters for the Full Stack
Later, you’ll test:
API calls from React to your NestJS backend
Error handling and loading states
Protected routes after authentication
End-to-end flows using Playwright
This small foundation prepares you for the much deeper testing work in Part 5.
Tests don’t slow you down — they let you ship fearlessly.
10. Wrap-Up
In this post, you learned how to:
Set up Jest + React Testing Library
Test components, props, events, and forms
Simulate real user interactions
Assert UI behavior safely
Build a testing mindset early in development
You now have all core skills from Part 1: components, props, state, events, forms, and testing.
Next, we move to Part 2 — Talking to Servers: Fetch, Forms, CRUD, UX State, where your React skills start interacting with real data over REST APIs.
Related
Part 0.5 — Git, GitHub, and Version Control for Teams
Great developers don’t just write code — they manage it. Learn how Git and GitHub shape teamwork, history, and CI/CD integration in professional full-stack environments.
Part 0.1 — Setting Up a Modern Dev Environment
Before you write a single line of React or NestJS code, you need a robust development environment. In this guide, we’ll set up Node, pnpm, PostgreSQL, and VS Code the right way—like a professional full-stack engineer.
Comments