This content originally appeared on DEV Community and was authored by Learcise
When developing with React, have you ever wondered:
“Where should I even start with testing?”
This article walks you through unit tests, integration tests, end-to-end (E2E) tests, snapshot tests, and test doubles,
focusing on tools like Jest, React Testing Library, and Playwright.
🧠 Why Write Tests?
Writing tests isn’t just about reducing bugs.
It brings lasting benefits to your development team:
- ✅ Enables safe refactoring
- ✅ Serves as living documentation for expected behavior
- ✅ Helps maintain code quality over time
React apps often deal with complex state and side effects (like useEffect), making them prone to hidden bugs.
That’s why having a solid testing strategy is crucial.
⚙️ The Three Layers of Testing: Unit, Integration, and E2E
🔹 Unit Tests
Unit tests focus on the smallest pieces of your application — functions, components, or custom hooks.
They check whether a given input produces the expected output.
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('increments the count by 1', () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
🔹 Integration Tests
Integration tests verify how multiple components or functions work together.
For example: form input → validation → API call.
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Form from './Form';
test('calls API on form submit', async () => {
const mockSubmit = jest.fn();
render(<Form onSubmit={mockSubmit} />);
fireEvent.change(screen.getByLabelText(/Name/), { target: { value: 'Taro' } });
fireEvent.click(screen.getByText('Submit'));
await waitFor(() => expect(mockSubmit).toHaveBeenCalledWith('Taro'));
});
🔹 End-to-End (E2E) Tests
E2E tests simulate real user interactions in the browser, covering the entire app flow.
Common tools include Playwright and Cypress.
// login.e2e.ts
import { test, expect } from '@playwright/test';
test('logs in and redirects to the dashboard', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
🧪 The Role of Jest and React Testing Library
✅ What is Jest?
Jest is a testing framework developed by Facebook.
It provides everything you need for testing JavaScript apps:
- Test runner
- Mocks and spies
- Snapshot testing
It’s commonly used together with React Testing Library.
✅ What is React Testing Library?
Instead of directly manipulating the DOM, React Testing Library focuses on testing the app from the user’s perspective — by simulating clicks, typing, and other interactions.
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('increments the counter when button is clicked', () => {
render(<Counter />);
fireEvent.click(screen.getByText('+'));
expect(screen.getByText('1')).toBeInTheDocument();
});
📸 What Is a Snapshot Test?
A snapshot test saves a “picture” of a component’s rendered output and compares it on future test runs to detect unexpected UI changes.
It’s powered by Jest’s toMatchSnapshot() function.
✅ Example
// Button.tsx
export const Button = ({ label }: { label: string }) => {
return <button>{label}</button>;
};
// Button.test.tsx
import renderer from 'react-test-renderer';
import { Button } from './Button';
test('matches the snapshot', () => {
const tree = renderer.create(<Button label="Send" />).toJSON();
expect(tree).toMatchSnapshot();
});
When you run the test for the first time, Jest stores a snapshot like this under __snapshots__:
exports[`Button snapshot 1`] = `
<button>
Send
</button>
`;
If the component’s output changes later, Jest will show a diff:
- <button>
+ <button className="primary">
✅ If the change is intentional, update the snapshot with npm test -- -u.
⚖️ Pros and Cons
| Type | Description |
|---|---|
| ✅ Pros | Catches unintended UI changes, easy to set up |
| ⚠️ Cons | Can produce noisy diffs, not suitable for dynamic elements |
| 💡 Best Practice | Use for small, stable UI parts only |
🧩 Using React Testing Library for Snapshots
import { render } from '@testing-library/react';
import Button from './Button';
test('Button matches snapshot', () => {
const { asFragment } = render(<Button label="Send" />);
expect(asFragment()).toMatchSnapshot();
});
🧱 Understanding Test Doubles: Mocks, Stubs, Fakes, and Dummies
While snapshot tests detect visual changes,
test doubles help you “recreate and control” external behavior to make your tests safe and reliable.
🔸 What Is a Test Double?
In React testing, components often depend on external systems — APIs, databases, or browser APIs.
Using real ones makes tests slow and flaky.
A test double is a fake replacement for those dependencies that lets you simulate external behavior safely.
🧩 Example: When API Responses Affect the UI
Let’s say you have a component that fetches user data and displays it.
💡 Original Code
// UserInfo.tsx
import { useEffect, useState } from 'react';
import { fetchUser } from './api';
export const UserInfo = () => {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser);
}, []);
if (!user) return <p>Loading...</p>;
return <p>Hello, {user.name}!</p>;
};
If you test this with a real API:
- The test fails when the API is down
- It takes time due to network latency
- Results vary depending on live data
✅ Using a Test Double
import { render, screen, waitFor } from '@testing-library/react';
import { UserInfo } from './UserInfo';
import { fetchUser } from './api';
jest.mock('./api'); // Mock the API module
test('displays the user name', async () => {
fetchUser.mockResolvedValue({ name: 'Hanako' });
render(<UserInfo />);
await waitFor(() =>
expect(screen.getByText('Hello, Hanako!')).toBeInTheDocument()
);
});
Here, the API response is simulated inside the test.
No real network requests are made, and results are always consistent.
🧭 Why Use Test Doubles?
| Goal | Benefit |
|---|---|
| Isolate external dependencies | No need for real APIs or databases |
| Speed up tests | Responses are instant |
| Ensure consistency | Same result every time |
| Reproduce special cases | e.g. simulate API errors easily |
🎯 Key idea: Test doubles let you mimic real-world behavior and take full control of the testing environment.
🧩 Types of Test Doubles and Examples
| Type | Definition | Use Case | Example (React/Jest) |
|---|---|---|---|
| Mock | A fake object that tracks how it’s used (calls, args) | Verify function calls | js\nconst mockFn = jest.fn();\nfireEvent.click(button);\nexpect(mockFn).toHaveBeenCalledTimes(1);\n |
| Stub | Returns fixed values; doesn’t record history | Simulate API responses | js\njest.spyOn(api, 'fetchUser').mockResolvedValue({ name: 'Taro' });\n |
| Fake | A lightweight working version of a dependency | Replace local DB or storage | js\nclass FakeLocalStorage {\n setItem(k,v){this.store[k]=v;}\n getItem(k){return this.store[k];}\n}\n |
| Dummy | Placeholder objects that aren’t actually used | Required arguments | js\nrender(<UserCard user={null} onClick={() => {}} />);\n |
| Spy | Observes calls to real functions | Monitor logs or side effects | js\nconst spy = jest.spyOn(console, 'log');\nlogMessage('Hello');\nexpect(spy).toHaveBeenCalledWith('Hello');\n |
🧩 Designing a Testing Strategy
🔺 The Testing Pyramid
▲ E2E Tests (few)
▲▲ Integration Tests (some)
▲▲▲ Unit Tests (many)
- Unit tests: small, fast, and plentiful
- Integration tests: verify key workflows
- E2E tests: ensure the entire user experience
🧠 Continuous Testing
- Automate tests in CI/CD (e.g., GitHub Actions)
- Use snapshot tests to catch unintended UI changes early
🏁 Summary
| Test Type | Purpose | Tools |
|---|---|---|
| Unit Test | Verify functions/components individually | Jest / React Testing Library |
| Integration Test | Test multiple units together | Jest / RTL |
| E2E Test | Simulate real user flows | Playwright / Cypress |
| Snapshot Test | Detect unintended UI changes | Jest |
| Test Double | Simulate and control external dependencies | Jest.mock / Spy / Stub |
🧑💻 Final Thoughts
Testing doesn’t slow development down —
it’s a tool for building safely and refactoring with confidence.
Start small with unit tests, then add snapshots and test doubles as your project grows.
This content originally appeared on DEV Community and was authored by Learcise
Learcise | Sciencx (2025-10-07T02:26:40+00:00) 🧩 A Complete Guide to React Testing: From Unit Tests to E2E, Snapshots, and Test Doubles. Retrieved from https://www.scien.cx/2025/10/07/%f0%9f%a7%a9-a-complete-guide-to-react-testing-from-unit-tests-to-e2e-snapshots-and-test-doubles/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.