🧪 React Testing Patterns: Your Safety Net for Awesome Apps
The Story: The Quality Detective Agency 🕵️
Imagine you’re building a LEGO castle. Before showing it to your friends, you’d want to make sure:
- All the pieces stick together properly
- The doors actually open
- It doesn’t fall apart when someone touches it
Testing in React is exactly like being a LEGO inspector! You check every piece of your app to make sure it works perfectly before real users see it.
Today, we’ll learn the 7 secret testing powers that every React detective needs:
graph LR A["🎯 Testing Patterns"] --> B["👆 User Events"] A --> C["⏳ Async Testing"] A --> D["📝 Form Testing"] A --> E["🎭 Mocking"] A --> F["🪝 Testing Hooks"] A --> G["🌍 Testing Context"] A --> H["🔗 Integration Testing"]
1. 👆 User Event Simulation
What is it?
When you click a button in your app, something happens. User event simulation is like having a robot friend who clicks buttons for you to check if they work!
The Analogy
Think of it like a remote control car. You press the forward button → the car moves forward. Testing user events means checking: “When I press forward, does the car REALLY go forward?”
Simple Example
import { render, screen } from
'@testing-library/react';
import userEvent from
'@testing-library/user-event';
function Counter() {
const [count, setCount] =
useState(0);
return (
<button
onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
The Test:
test('clicking adds 1', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole(
'button', { name: /count: 0/i }
);
await user.click(button);
expect(button).toHaveTextContent(
'Count: 1'
);
});
🎯 Key Events You Can Simulate
| Event | What it does |
|---|---|
click() |
Clicks a button |
type() |
Types text |
clear() |
Clears input |
selectOptions() |
Picks from dropdown |
tab() |
Moves focus |
2. ⏳ Async Testing
What is it?
Sometimes things don’t happen instantly. Like waiting for a pizza delivery! Async testing helps us wait for slow things (like data from the internet) before checking if everything worked.
The Analogy
Imagine ordering food online:
- You click “Order” 🍕
- You wait… ⏳
- Food arrives! 🎉
We need to wait before checking if the food is right!
Simple Example
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
if (!user) return <p>Loading...</p>;
return <h1>{user.name}</h1>;
}
The Test:
test('shows user after loading',
async () => {
render(<UserProfile userId="1" />);
// First, we see "Loading..."
expect(screen.getByText('Loading...'))
.toBeInTheDocument();
// Wait for the name to appear!
const userName = await screen.findByRole(
'heading', { name: 'Alice' }
);
expect(userName).toBeInTheDocument();
});
🔑 The Magic Words
| Method | When to use |
|---|---|
findBy* |
Wait for element to appear |
waitFor() |
Wait for any condition |
waitForElementToBeRemoved() |
Wait for loading to go away |
3. 📝 Form Testing
What is it?
Forms are like questionnaires. Users fill them out, click submit, and something happens. Form testing checks if all the questions work and the answers go to the right place!
The Analogy
Think of a treasure hunt form:
- Write your name ✍️
- Pick your team 🎯
- Click “Start Hunt” 🚀
We need to make sure each step works perfectly!
Simple Example
function LoginForm({ onLogin }) {
const [email, setEmail] = useState('');
const [password, setPassword] =
useState('');
const handleSubmit = (e) => {
e.preventDefault();
onLogin({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) =>
setEmail(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) =>
setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}
The Test:
test('submits login form', async () => {
const mockLogin = jest.fn();
const user = userEvent.setup();
render(<LoginForm onLogin={mockLogin} />);
await user.type(
screen.getByPlaceholderText('Email'),
'test@example.com'
);
await user.type(
screen.getByPlaceholderText('Password'),
'secret123'
);
await user.click(
screen.getByRole('button',
{ name: 'Login' })
);
expect(mockLogin).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'secret123'
});
});
4. 🎭 Mocking
What is it?
Mocking is like using pretend props in a play. Instead of calling the real internet or database, we use fake versions that we control!
The Analogy
Imagine practicing for a talent show:
- Real show: Thousands of people watching 😰
- Practice: Just your stuffed animals 🧸
Mocking lets us practice with “stuffed animals” instead of real audiences!
Types of Mocking
graph TD A["🎭 Mocking Types"] --> B["📦 Mock Functions"] A --> C["🌐 Mock API Calls"] A --> D["📚 Mock Modules"]
Simple Example - Mock Functions
test('calls onClick when clicked',
async () => {
// Create a "pretend" function
const mockFn = jest.fn();
const user = userEvent.setup();
render(
<Button onClick={mockFn}>
Click me
</Button>
);
await user.click(
screen.getByRole('button')
);
// Check if our pretend
// function was called
expect(mockFn)
.toHaveBeenCalledTimes(1);
});
Mocking API Calls
// Mock the fetch function
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(
{ name: 'Test User' }
)
})
);
test('loads user data', async () => {
render(<UserProfile />);
await screen.findByText('Test User');
expect(fetch).toHaveBeenCalledWith(
'/api/user'
);
});
5. 🪝 Testing Hooks
What is it?
Hooks are like magic spells in React. Testing hooks means checking if our spells work correctly, even when we’re not inside a component!
The Analogy
Think of hooks like a recipe:
- The recipe (hook) has steps
- We test if following the steps gives us the right cake!
Simple Example
// Our custom hook
function useCounter(initial = 0) {
const [count, setCount] =
useState(initial);
const increment = () =>
setCount(c => c + 1);
const decrement = () =>
setCount(c => c - 1);
return { count, increment, decrement };
}
The Test:
import { renderHook, act } from
'@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(
() => useCounter(5)
);
// Initial value
expect(result.current.count).toBe(5);
// Increment (must wrap in act!)
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(6);
});
🚨 Golden Rule
Always wrap state changes in act() - it tells React “Hey, something is changing!”
6. 🌍 Testing Context
What is it?
Context is like a family group chat. Everyone in the family can see messages without passing them one by one. Testing context means checking if everyone gets the right messages!
The Analogy
Imagine a theme park with a dress code:
- The park says “Wear blue today” (Context)
- All visitors automatically know to wear blue
- We test if everyone really wears blue!
Simple Example
// Theme Context
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] =
useState('light');
return (
<ThemeContext.Provider
value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme } =
useContext(ThemeContext);
return (
<button className={theme}>
I am {theme}
</button>
);
}
The Test:
// Create a wrapper for testing
function renderWithTheme(component) {
return render(
<ThemeProvider>
{component}
</ThemeProvider>
);
}
test('button shows theme', () => {
renderWithTheme(<ThemedButton />);
expect(screen.getByRole('button'))
.toHaveTextContent('I am light');
});
Testing Theme Changes
test('theme can be changed', async () => {
const user = userEvent.setup();
renderWithTheme(
<>
<ThemeToggle />
<ThemedButton />
</>
);
await user.click(
screen.getByRole('button',
{ name: 'Toggle Theme' })
);
expect(screen.getByText('I am dark'))
.toBeInTheDocument();
});
7. 🔗 Integration Testing
What is it?
Integration testing is like checking if all the LEGO pieces work together, not just one piece at a time. We test real user journeys from start to finish!
The Analogy
Think of making a sandwich:
- Unit test: Is the bread fresh? ✓
- Unit test: Is the cheese good? ✓
- Integration test: Does the whole sandwich taste good together? 🥪
Simple Example
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos,
{ text: input, done: false }]);
setInput('');
}
};
const toggleTodo = (index) => {
const newTodos = [...todos];
newTodos[index].done =
!newTodos[index].done;
setTodos(newTodos);
};
return (
<div>
<input
value={input}
onChange={(e) =>
setInput(e.target.value)}
placeholder="Add todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, i) => (
<li
key={i}
onClick={() => toggleTodo(i)}
style={{
textDecoration: todo.done
? 'line-through' : 'none'
}}>
{todo.text}
</li>
))}
</ul>
</div>
);
}
The Integration Test:
test('full todo workflow', async () => {
const user = userEvent.setup();
render(<TodoApp />);
// 1. Add a todo
await user.type(
screen.getByPlaceholderText('Add todo'),
'Buy milk'
);
await user.click(
screen.getByRole('button', { name: 'Add' })
);
// 2. Check it appears
expect(screen.getByText('Buy milk'))
.toBeInTheDocument();
// 3. Add another
await user.type(
screen.getByPlaceholderText('Add todo'),
'Walk dog'
);
await user.click(
screen.getByRole('button', { name: 'Add' })
);
// 4. Complete first todo
await user.click(
screen.getByText('Buy milk')
);
// 5. Verify it's crossed out
expect(screen.getByText('Buy milk'))
.toHaveStyle(
'text-decoration: line-through'
);
// 6. Second todo still normal
expect(screen.getByText('Walk dog'))
.not.toHaveStyle(
'text-decoration: line-through'
);
});
🎯 Quick Summary
| Pattern | What it Tests | When to Use |
|---|---|---|
| User Events | Clicks, typing, selections | Any user interaction |
| Async | Loading states, API data | When waiting for data |
| Forms | Input, validation, submit | Any form component |
| Mocking | Dependencies, APIs | External services |
| Hooks | Custom hook logic | Reusable state logic |
| Context | Shared state | Theme, auth, app state |
| Integration | Full workflows | Complete user journeys |
🚀 You’re Now a Testing Detective!
Remember: Good tests = Confident deployments!
Every time you write a test, you’re building a safety net. When you change code later, your tests catch any broken pieces instantly.
That’s the superpower of testing patterns! 🦸♂️
graph TD A["Write Code"] --> B["Write Tests"] B --> C["Run Tests ✅"] C --> D["Deploy with Confidence 🚀"] D --> E["Happy Users 😊"]
Pro Tip: Start with integration tests for user flows, then add unit tests for tricky logic. You’ll catch bugs before your users do! 🐛🔍
