PaoloJulian.dev - Article

go to article list

Mastering React Testing: A Comprehensive Checklist of What to Test

Paolo Vincent Julian
Mastering React Testing: A Comprehensive Checklist of What to Test banner

Banner - Mastering React Testing: A Comprehensive Checklist of What to Test

LAST UPDATED 

Hello there,

Let me take you on a journey through my personal experience with unit testing in React applications. It's a story of growth, where I once grappled with the concepts of unit tests, end-to-end tests and feature tests, wondering what on earth the difference was. You see, unit testing in the world of frontend development can be a bit like navigating a maze in the dark.

Let's dive in and unravel the fascinating world of React unit testing together.

TLDR; skip to testing examples, click here

Table of Contents

  1. Introduction
  2. What are the things to test?
  3. Examples
  4. Bonus: Storybook, Showcasing Component and Visual Testing.
  5. Conclusion

Introduction

I vividly remember those days when I questioned what exactly I should be testing. Back-end testing seemed so straightforward in comparison. But as I delved deeper into the intricacies of front-end development, I realized that testing React components opens up a whole new world of possibilities.

Unit tests, as it turned out, became my allies in crafting robust and dependable React code. They offered a safety net, albeit not a 100% refactor-proof one, that allowed me to make changes with more confidence. It's incredible how a few well-placed unit tests can bring peace of mind to your development process.

What is Unit Testing?

Unit testing is like testing individual puzzle pieces before putting them together to complete a puzzle. In programming, it means checking small parts (or units) of your code to make sure they work correctly on their own.

Imagine you're building a robot. Before assembling the entire robot, you'd want to test each component—like its arms, legs, and sensors—to ensure they function properly. Unit testing in coding is similar; it checks that each part of your code, like functions or small sections, does its job as expected.

Here's an important rule: When you're unit testing, you're only testing one specific part of your code. For example, if you have a component called Button.tsx, you should only test Button.tsx itself. You don't test other files or parts of your code all at once.

If your code uses other things (we call them dependencies), you don't test them in your Button.tsx test. Instead, you create pretend (mock) versions of those things just for the test. This way, you can focus on making sure Button.tsx works perfectly without worrying about the other stuff.

Why is it important?

Unit testing plays a crucial role in ensuring the quality and stability of a React application. Here's why it's important:

Refactor with More Confidence: Unit testing provides you with the confidence to make changes to your code, especially when you need to refactor or improve it. For example, if a new feature is introduced that affects your component, unit testing ensures that your component remains functional and doesn't break other parts of your code.

Test Different Scenarios Without Manual Testing: Unit testing allows you to test various scenarios and edge cases automatically, without the need for manual testing. This means you can quickly verify that your code works correctly under different conditions, saving you time and effort.

Proof the Styling of Your App: Unit testing can also help ensure that the styling of your application remains consistent and error-free. By using feature snapshot testing, you can capture and compare snapshots of your components' rendered appearance. This helps you detect any unexpected visual changes, ensuring that your app maintains its desired look and feel.

Documentation: Well-written unit tests can serve as documentation, helping developers understand how a component or function is intended to work.

Common Tools Used for Testing in React.js

1. Jest 2. React Testing Library 3. Enzyme 4. Mocha

What are the things to test?

When testing React applications, you want to cover a wide range of scenarios to ensure the functionality, reliability, and quality of your components. Here's a comprehensive list of things to test:

  1. Rendering and Initial State
  2. Props and State Changes
  3. User Interaction
  4. Asynchronous Behavior
  5. Component Lifecycle
  6. Snapshot Testing
  7. Event Handlers
  8. Conditional Rendering
  9. Mocking and Spying
  10. Props Validation
  11. Error Handling
  12. Redux Integration (if applicable)
  13. Router Integration (if applicable)
  14. Accessibility:
  15. Context and Hooks (if applicable)
  16. Component Composition
  17. Code Coverage
  18. Edge Cases
  19. Performance
  20. Cross-Browser Compatibility

Examples:

Here is a compilation of common test cases that you can use as a starting point for testing your React components. By studying these examples, you'll gain insight into how to structure your tests and ensure the reliability of your React application. Let's explore these practical test cases to enhance your testing skills and build more robust components.

1. Testing prop changes

jsx
// Counter.test.js
import React from 'react';
import { render } from '@testing-library/react';
import Counter from './Counter';

describe('Counter', () => {
  it('renders the count prop', () => {
    const { getByText, rerender } = render(<Counter count={0} />);

    // Initial render with count prop 0
    expect(getByText('Count: 0')).toBeInTheDocument();

    // Re-render with count prop 5
    rerender(<Counter count={5} />);
    expect(getByText('Count: 5')).toBeInTheDocument();
  });
});

2. Testing user interaction

jsx
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import ClickCounter from './ClickCounter';

describe('ClickCounter', () => {
  it('increments the count when clicked', () => {
    const { getByText } = render(<ClickCounter />);
    const countElement = getByText('Count: 0');
    const button = getByText('Click Me');

    fireEvent.click(button);
    expect(countElement).toHaveTextContent('Count: 1');

    fireEvent.click(button);
    expect(countElement).toHaveTextContent('Count: 2');
  });
});

3. Testing asynchronous requests

jsx
// fetchData.js
export const fetchData = async () => {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
};

// DataDisplay.js
import React, { useState, useEffect } from 'react';
import { fetchData } from './fetchData';

const DataDisplay = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchDataAsync = async () => {
      const fetchedData = await fetchData();
      setData(fetchedData);
    };

    fetchDataAsync();
  }, []);

  return <div>{data ? data.message : 'Loading...'}</div>;
};

export default DataDisplay;
jsx
// DataDisplay.test.js
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import DataDisplay from './DataDisplay';

// Mock the fetchData function
jest.mock('./fetchData', () => ({
  fetchData: jest.fn(() => Promise.resolve({ message: 'Hello, World!' })),
}));

describe('DataDisplay', () => {
  it('displays the fetched message', async () => {
    const { getByText } = render(<DataDisplay />);

    // Wait for fetchData to resolve
    await waitFor(() => expect(getByText('Hello, World!')).toBeInTheDocument());
  });
});

4. Testing snapshots

Snapshots can indeed be confusing, but they play a crucial role in your testing process. Let's explore why snapshots are essential:

Detecting Code Changes: Snapshots become invaluable when you make changes, such as altering a class name. If you modify a component's structure or appearance, the snapshot will detect any discrepancies and signal an error.

Code Change Example: Consider this scenario: You've made a code change that includes altering a class name. Without snapshots, you might not immediately notice any issues. However, snapshots will catch such changes and highlight them for your attention.

Snapshot error
Snapshot error

What if you want to update the component: Sometimes, you may intentionally want to change a component's output. In such cases, you can update the snapshot to reflect the new expected output with this command:

bash
yarn test --updateSnapshot

Here is the example test code:

tsx
import React from 'react';
import { render } from '@testing-library/react';
import Header from './Header';

describe('TESTING Header Snapshot', () => {
  it('matches the snapshot', () => {
    const { asFragment } = render(<Header title="My App" />);
    expect(asFragment()).toMatchSnapshot();
  });
});

5. Testing Components with Custom Hooks

Testing components with custom hooks can sometimes be tricky. Here are some tricks to use:

  • Use Mocks: Employ jest.mock to simulate the behavior of custom hooks.
  • Add a Reference Variable: Add a reference variable that is accessible for functions returned by a hook.
  • Utilize jest.mocked: Utilize jest.mocked to obtain a "mocked" copy of the imported hook.
tsx
import { fireEvent, render, screen } from '@testing-library/react';
import HeadingLink from '@/_components/buttons/heading-link';
import { DATA_TEST } from '@/_components/buttons/heading-link/heading-link.constants';
import useCopy from '@/_hooks/use-copy';

// Mock the path of the hook
jest.mock('@/_hooks/use-copy', () => {
  // Add default values
  return jest.fn(() => ({
    isCopied: false,
    handleClickCopy: jest.fn(),
  }));
});

// Get a copy of the mocked "useCopy" hook.
const mockedUseCopy = jest.mocked(useCopy);

// ...other tests

    describe('WHEN the popover link is clicked', () => {
      it('THEN it should call the handleClickCopy function', () => {
        // Create a linked jest.fn() so you can listen to it.
        const mockedHandleClickCopy = jest.fn();
        mockedUseCopy.mockReturnValueOnce({
          isCopied: false,
          handleClickCopy: mockedHandleClickCopy,
        });
        renderHeadingLink();

        const popoverLink = screen.getByTestId(DATA_TEST.popover);

        fireEvent.click(popoverLink);

        // Here is when you need the referenced jest function.
        expect(mockedHandleClickCopy).toHaveBeenCalledTimes(1);
      });
    });

    describe('WHEN the popover link is copied', () => {
      it('THEN it should contain the text "Copied"', () => {
        mockedUseCopy.mockReturnValueOnce({
          isCopied: true,
          handleClickCopy: jest.fn(),
        });

        renderHeadingLink();

        const popoverLink = screen.getByTestId(DATA_TEST.popover);

        expect(popoverLink).toHaveTextContent(/Copied!/i);
      });
    });

// ...the rest of the tests

6. Testing Custom Hooks

Testing custom hooks, we should remember renderHook, then to run a function inside a hook, we use act to fire a hook event.

tsx
import { act, renderHook } from '@testing-library/react';
import useCopy from '@/_hooks/use-copy';

const writeText = jest.fn()
Object.assign(navigator, {
  clipboard: {
    writeText,
  },
});

describe('TESTING useCopy custom hook', () => {
  describe('GIVEN the link and timeout', () => {
    const link = 'https://www.paolojulian.dev/blogs/unit-testing#unit-test'

    describe('WHEN the useCopy is called', () => {
      it('THEN it should return default state', () => {
        const { result } = renderHook(useCopy, { initialProps: { link } })
        expect(result.current.isCopied).toBe(false);
      });
    });

    describe('WHEN the handleClickCopy is called', () => {
      it('THEN it should assign the link to the navigator', () => {
        const { result } = renderHook(useCopy, { initialProps: { link, timeout_ms: timeout } })
        act(() => result.current.handleClickCopy())
        expect(navigator.clipboard.writeText).toHaveBeenCalled();
      });
      it('THEN it should return isCopied as true', () => {
        const { result } = renderHook(useCopy, { initialProps: { link, timeout_ms: timeout } })
        act(() => result.current.handleClickCopy())
        expect(result.current.isCopied).toBe(true);
      });
      it('THEN it should return isCopied as false after the timer is called ', () => {
        const { result } = renderHook(useCopy, { initialProps: { link, timeout_ms: timeout } })
        jest.useFakeTimers();
        act(() => result.current.handleClickCopy())
        act(() => jest.runAllTimers())
        expect(result.current.isCopied).toBe(false);
      });
    });

  });
});

Bonus: Storybook, Showcasing Component and Visual Testing.

Storybook is a valuable tool for showcasing your components in isolation and performing visual testing. It provides an environment to build a library of interactive components, enabling developers to focus on individual elements, test different scenarios, and ensure visual consistency. Here's a concise look at Storybook's role in your development workflow, with an emphasis on visual testing.

What is Storybook? Storybook is an open-source tool that facilitates component development in isolation. It allows you to create interactive "stories" for each component, demonstrating their behavior, appearance, and responsiveness independently of the full application.

You can see video demos in their website.

What Storybook Can Do: 1. Change Props: Easily manipulate and preview component behavior by altering props. 2. Test Component States: Simulate loading and error states for thorough testing. 3. Isolate Components: Develop and test components individually, enhancing efficiency. 4. Visual Testing: Detect unintended visual changes using Storybook snapshots. 5. Collaboration: Integrate with pull requests to visualize and review visual changes.

and many more...

Conclusion

Effective testing is the cornerstone of reliable React development. From unit and integration testing to visual and performance testing, each approach strengthens your codebase. By catching bugs early, ensuring smooth interactions, and optimizing performance, you create a resilient foundation for innovation.

As you evolve, your testing strategy will adapt too. Embrace continuous learning, staying attuned to best practices and emerging trends. With every test, you shape your application's success and confidently navigate the dynamic landscape of React development.

Happy coding and testing!

TAGS:

#testing

#best-practices

go to article list