Testing Patterns

Back

Loading concept...

🧪 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:

  1. You click “Order” 🍕
  2. You wait… ⏳
  3. 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! 🐛🔍

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.