go to article list
Banner - Mastering React Testing: A Comprehensive Checklist of What to Test
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
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.
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.
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.
1. Jest 2. React Testing Library 3. Enzyme 4. Mocha
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:
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.
// 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();
});
});
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');
});
});
// 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;
// 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());
});
});
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.
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:
yarn test --updateSnapshot
Here is the example test code:
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();
});
});
Testing components with custom hooks can sometimes be tricky. Here are some tricks to use:
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
Testing custom hooks, we should remember renderHook, then to run a function inside a hook, we use act to fire a hook event.
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);
});
});
});
});
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...
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
Having trouble updating branches that is dependent to one another? Enter git-branch-updater
KEEP IT SIMPLE STUPID! EP1 - Pomodoro
The Right Way to Make Component Variants using Tailwind
Essential Tools and Libraries for Daily Web Development Works
How to Send Emails in Next.js 13 Using the New App API Route
Mastering React Testing: A Comprehensive Checklist of What to Test
Mastering Commit Messages: Best Practices and Examples
How to Integrate Contentful to Next 13
Real-Life Applications of Design Patterns in Your Daily React Code