React Unit Testing: A Complete Guide with Jest & Testing Library - Part 3: Testing Components with Jest
João
- •
- 10 MIN TO READ
React Unit Testing: A Complete Guide with Jest & Testing Library - Part 2: Configuring Your React Project for Unit Testing
📖 8 min read | 💻 Intermediate | ⚡ With Code Examples
Prerequisites: This guide assumes you have a React project already set up. If you haven't created your React project yet or need help with the initial setup, check out my guide 🚀 React in 2025: Building Modern Apps with Vite - A Developer's Guide, then come back here.
First, let's install the necessary dependencies for our testing environment:
1npm install --save-dev @testing-library/react@latest @testing-library/jest-dom@latest @testing-library/user-event@latest jest@latest jest-environment-jsdom@latest @babel/preset-react @babel/preset-env babel-jest
2
Let's set up the essential configuration files:
1// jest.config.js
2export default {
3 testEnvironment: "jsdom",
4 setupFilesAfterEnv: ["<rootDir>/src/setupTests.js"],
5 moduleNameMapper: {
6 // Handle CSS imports (with CSS modules)
7 "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
8
9 // Handle CSS imports (without CSS modules)
10 "^.+\\.(css|sass|scss)$": "<rootDir>/__tests__/mocks/styleMock.js",
11
12 // Handle image imports
13 "^.+\\.(jpg|jpeg|png|gif|webp|svg)$":
14 "<rootDir>/__tests__/mocks/fileMock.js",
15
16 // Handle path aliases
17 "^@/(.*)$": "<rootDir>/src/$1",
18 },
19 transform: {
20 "^.+\\.(js|jsx|ts|tsx)$": "babel-jest",
21 },
22 testMatch: [
23 "**/__tests__/**/*.[jt]s?(x)",
24 "**/?(*.)+(spec|test).[jt]s?(x)",
25 "!**/__tests__/mocks/*.[jt]s?(x)",
26 "!**/__tests__/helpers/*.[jt]s?(x)",
27 ],
28 coverageThreshold: {
29 global: {
30 branches: 95,
31 functions: 95,
32 lines: 95,
33 statements: 95,
34 },
35 },
36};
37
1// babel.config.json
2{
3 "presets": [
4 "@babel/preset-env",
5 ["@babel/preset-react", { "runtime": "automatic" }]
6 ]
7}
8
1// src/setupTests.js
2import "@testing-library/jest-dom";
3import { cleanup } from "@testing-library/react";
4
5// Add custom jest matchers
6expect.extend({
7 // Add your custom matchers here
8});
9
10// Global test setup
11beforeAll(() => {
12 // Your global setup code
13});
14
15afterAll(() => {
16 // Your global cleanup code
17 cleanup();
18});
19
1// __tests__/mocks/fileMock.js
2export default "test-file-stub";
3
4// __tests__/mocks/styleMock.js
5export default {};
6
7// __tests__/mocks/windowMock.js
8Object.defineProperty(window, "localStorage", {
9 value: {
10 getItem: jest.fn(),
11 setItem: jest.fn(),
12 removeItem: jest.fn(),
13 clear: jest.fn(),
14 },
15});
16
1npm install eslint-plugin-jest
2
1// eslint.config.js
2
3// ...others imports
4import pluginJest from "eslint-plugin-jest";
5
6export default [
7 // ...others configs
8 plugins: {
9 // ...others plugins
10 jest: pluginJest,
11 },
12 rules: {
13 // ...others rules
14 "jest/no-disabled-tests": "warn",
15 "jest/no-focused-tests": "error",
16 "jest/no-identical-title": "error",
17 "jest/prefer-to-have-length": "warn",
18 "jest/valid-expect": "error",
19 }
20]
21
Your configuration should look like this:
1import js from "@eslint/js";
2import globals from "globals";
3import react from "eslint-plugin-react";
4import reactHooks from "eslint-plugin-react-hooks";
5import reactRefresh from "eslint-plugin-react-refresh";
6import pluginJest from "eslint-plugin-jest";
7
8export default [
9 { ignores: ["dist"] },
10 {
11 files: ["**/*.{js,jsx}"],
12 languageOptions: {
13 ecmaVersion: 2020,
14 globals: {
15 ...globals.browser,
16 ...pluginJest.environments.globals.globals,
17 },
18 parserOptions: {
19 ecmaVersion: "latest",
20 ecmaFeatures: { jsx: true },
21 sourceType: "module",
22 },
23 },
24 settings: { react: { version: "18.3" } },
25 plugins: {
26 react,
27 "react-hooks": reactHooks,
28 "react-refresh": reactRefresh,
29 jest: pluginJest,
30 },
31 rules: {
32 ...js.configs.recommended.rules,
33 ...react.configs.recommended.rules,
34 ...react.configs["jsx-runtime"].rules,
35 ...reactHooks.configs.recommended.rules,
36 "react/jsx-no-target-blank": "off",
37 "react-refresh/only-export-components": [
38 "warn",
39 { allowConstantExport: true },
40 ],
41 "jest/no-disabled-tests": "warn",
42 "jest/no-focused-tests": "error",
43 "jest/no-identical-title": "error",
44 "jest/prefer-to-have-length": "warn",
45 "jest/valid-expect": "error",
46 },
47 },
48];
49
Add these scripts to your package.json:
1{
2 "scripts": {
3 "test": "jest",
4 "test:watch": "jest --watch",
5 "test:coverage": "jest --coverage",
6 "test:ci": "jest --ci --coverage"
7 }
8}
9
Here's a recommended structure for your test files:
1src/
2├── components/
3│ ├── TaskManager/
4│ │ ├── TaskManager.jsx
5│ │ ├── TaskManager.test.jsx
6├── __tests__/
7│ ├── helpers/
8│ │ └── test-utils.js
9│ ├── mocks/
10│ │ └── fileMock.js
11│ │ └── styleMock.js
12│ │ └── windowMock.js
13└── setupTests.js
14
Create helper functions to make testing easier:
1// src/test-utils.jsx
2import { render } from "@testing-library/react";
3import userEvent from "@testing-library/user-event";
4
5const customRender = (ui, options = {}) => {
6 const AllTheProviders = ({ children }) => {
7 return (
8 // Add your providers here (React Query, Redux, etc)
9 children
10 );
11 };
12
13 return {
14 user: userEvent.setup(),
15 ...render(ui, { wrapper: AllTheProviders, ...options }),
16 };
17};
18
19export { customRender as render };
20
1// Component test
2Component.test.jsx
3// or
4Component.spec.jsx
5
6// Hook test
7useHook.test.js
8
9// Utility test
10utility.test.js
11
1// src/__mocks__/windowMock.js
2Object.defineProperty(window, "localStorage", {
3 value: {
4 getItem: jest.fn(),
5 setItem: jest.fn(),
6 removeItem: jest.fn(),
7 clear: jest.fn(),
8 },
9});
10
11Object.defineProperty(window, "matchMedia", {
12 value: jest.fn().mockImplementation((query) => ({
13 matches: false,
14 media: query,
15 onchange: null,
16 addEventListener: jest.fn(),
17 removeEventListener: jest.fn(),
18 })),
19});
20
1// For components that use timeouts
2jest.useFakeTimers();
3
4// For fetch calls
5global.fetch = jest.fn();
6
7// For environment variables
8process.env.REACT_APP_API_URL = "http://test-api.com";
9
For GitHub Actions:
1# .github/workflows/test.yml
2name: Tests
3on: [push, pull_request]
4
5jobs:
6 test:
7 runs-on: ubuntu-latest
8 steps:
9 - uses: actions/checkout@v4
10 - uses: actions/setup-node@v4
11 with:
12 node-version: "20"
13 cache: "npm"
14
15 - name: Install dependencies
16 run: npm ci
17
18 - name: Run tests
19 run: npm run test:ci
20
This configuration provides:
Add these configurations for VS Code:
1// .vscode/launch.json
2{
3 "version": "0.2.0",
4 "configurations": [
5 {
6 "name": "Debug Jest Tests",
7 "type": "node",
8 "request": "launch",
9 "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest",
10 "args": [
11 "--runInBand",
12 "--watchAll=false",
13 "--testNamePattern",
14 "${jest.testNamePattern}",
15 "--runTestsByPath",
16 "${jest.testFile}"
17 ],
18 "cwd": "${workspaceRoot}",
19 "console": "integratedTerminal",
20 "internalConsoleOptions": "neverOpen",
21 "disableOptimisticBPs": true
22 },
23 {
24 "name": "Debug Current Test File",
25 "type": "node",
26 "request": "launch",
27 "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest",
28 "args": ["${fileBasename}", "--config", "jest.config.js"],
29 "console": "integratedTerminal",
30 "internalConsoleOptions": "neverOpen"
31 }
32 ]
33}
34