Module 1: Testing React - Web Unit 3 Sprint 11

Module Overview

In this module, you'll learn how to test React components effectively. You'll understand how to test components as their props change, use mocks in web application tests, and test asynchronous API calls within components.

Learning Objectives

Content

Testing React Components

Objective 1: Test React Components as Props Change

When testing React components, it's essential to test how components behave as props change. The React Testing Library provides the rerender() function that allows us to test components after props have been updated.

Testing Prop Changes with rerender()

To test components as props change, we need to:

  1. Render the component with initial props
  2. Test the initial state
  3. Use the rerender() function to update props
  4. Test the component with the new props
Example: Testing a Component with Props
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders friends from the API', () => {
  // Pass an empty list on first render (data hasn't arrived from API)
  const { rerender } = render(<App friends={[]} />);

  // Assert that there are no friends yet, because they are still arriving
  expect(screen.queryAllByTestId('friend')).toHaveLength(0);

  // Assert that a "fetching" message is showing
  expect(screen.queryByText('Fetching friends...')).toBeInTheDocument();

  // Rerender component with new props
  rerender(
    <App friends={[
      { id: 1, name: 'Alla' },
      { id: 2, name: 'Josh' }
    ]} />
  );

  // Assert that we have friends now, and that the "fetching" message is gone
  expect(screen.queryAllByTestId('friend')).toHaveLength(2);
  expect(screen.queryByText('Fetching friends...')).not.toBeInTheDocument();
});
Testing Content Not Rendered

Sometimes you need to assert that content is not present in the DOM. Use queryByRole or other queryBy assertions instead of getBy assertions, as these return null instead of throwing an error when an element isn't found.

// Assert content is not rendered
expect(screen.queryByRole("alert")).toBeNull();

// Or use .not with toBeInTheDocument()
expect(screen.queryByText('Error message')).not.toBeInTheDocument();

Using Mocks in Tests

Objective 2: Use Mocks in Web Application Tests

Mocks are essential for isolating the behavior of functions in your tests, especially when dealing with dependencies that would make testing difficult, such as random ID generators or API calls.

Why Use Mocks?

Mocks provide two key benefits:

  • They replace objects with simulations, allowing isolation of the specific function being tested
  • They act as "spies" to track calls to functions and the parameters passed in those calls
Example: Mocking a Module

Consider a component that uses the nanoid library to generate unique IDs:

import { nanoid } from "nanoid";

export const makeUser = (firstName, lastName) => {
  return {
    id: nanoid(),
    fullName: `${firstName} ${lastName}`
  };
};

Testing this would be challenging because nanoid() generates a random ID each time. We can mock the nanoid function to return a predictable value:

// At the top of your test file
jest.mock('nanoid', () => ({
  nanoid: () => 'abcde',
}));

test("generates a user with an id and a full name", () => {
  // Arrange
  const expected = { id: "abcde", fullName: "Peter Parker" };

  // Act
  const actual = makeUser("Peter", "Parker");

  // Assert
  expect(actual).toEqual(expected);
});

The mock replaces the real nanoid module with our controlled version, making the tests predictable and reliable.

Testing Async API Calls

Objective 3: Test Asynchronous API Calls in Components

Testing asynchronous operations such as API calls requires special handling to ensure your tests wait for those operations to complete before making assertions.

Testing Async Operations

React Testing Library provides the waitFor function to help test asynchronous operations. Combined with mocking, it allows you to test components that make API calls without actually waiting for real network requests.

Step-by-Step Approach for Testing Async Calls
  1. Import necessary libraries including waitFor
  2. Mock the API function
  3. Set up what the mock should return
  4. Render the component and trigger the async action (e.g., button click)
  5. Use waitFor to wait for DOM updates after the async operation
  6. Make assertions about the updated UI
Example: Testing a Component with API Call
import React from "react";
import { render, waitFor } from "@testing-library/react";
import userEvent from '@testing-library/user-event';
import { fetchDoggos as mockFetchDoggos } from "../api/fetchDoggos";
import Doggos from "./Doggos";

// Create mock before setting up test
jest.mock("../api/fetchDoggos");

test("renders dog images from API", async () => {
  // Mock resolved results
  mockFetchDoggos.mockResolvedValueOnce({
    message: [
      "https://images.dog.ceo/breeds/hound-afghan/n02088094_1003.jpg",
      "https://images.dog.ceo/breeds/hound-afghan/n02088094_1007.jpg",
      "https://images.dog.ceo/breeds/hound-afghan/n02088094_1023.jpg"
    ]
  });

  const { getByText, getAllByTestId } = render();
  const user = userEvent.setup();

  const fetchDoggosButton = getByText(/fetch doggos/i);
  await user.click(fetchDoggosButton);
  
  // Wait for the component to update after the async call
  await waitFor(() => {
    expect(getAllByTestId(/doggo-images/i)).toHaveLength(3);
  });
  
  // Verify the mock was called
  expect(mockFetchDoggos).toHaveBeenCalledTimes(1);
});

This approach allows you to test the full user flow, including the async API call, without actually making network requests.

Practice Activities

Additional Resources