13 - Time for Test
Types of Testing
-
- Manual Testing
- Testing the functionality that has been developed.
- Example: If we've developed a search bar, manual testing involves manually checking the search bar by entering queries.
- ❌ This method isn't very efficient for large applications because it's impractical to test every new feature manually. A single change can introduce bugs throughout the app since multiple components are interconnected.
-
- Automated Testing
- Writing test cases to verify functionality automatically. It includes:
-
Unit Testing Writing test cases for specific parts or isolated components.
-
Integration Testing Writing test cases for connected components, such as the menu page and cart page.
-
End-to-End Testing Writing test cases that simulate user interactions from entering the website to leaving it.
📦 Install Libraries
Note
If you are using Create React App or Vite, you can skip these installation steps as these packages are already included.
1. 🧪 React Testing Library
React Testing Library builds on top of DOM Testing Library by adding APIs for working with React components.
- Jest is used behind the scenes.
- Jest is a delightful JavaScript Testing Framework focused on simplicity.
- It works with projects using Babel, TypeScript, Node, React, Angular, Vue, and more.
- It allows you to test React components effectively.
Info
For more information, refer to the React Testing Library Documentation.
Install React Testing Library and DOM Testing Library:
2. ⚙️ Jest
Jest is required as React Testing Library relies on it.
Info
For more information, refer to the Jest Documentation.
Install Jest:
3. 🛠️ Extra Babel Libraries
Since we are using Babel as a bundler, install the following additional libraries:
Info
For more information, refer to the Using Babel with Jest Documentation.
Install Babel-related packages:
Configure Babel:
Create a babel.config.js
file in the root of your project:
// babel.config.js
const presets = [
[
"@babel/preset-env",
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns: "usage",
corejs: "3.6.4",
},
],
];
module.exports = { presets };
4. 🛡️ Configure Parcel
To avoid conflicts with Parcel's built-in Babel configuration, disable it by creating a .parcelrc
file.
Info
For more information, refer to the Parcel Configuration Documentation.
Create .parcelrc
:
// .parcelrc
{
"extends": "@parcel/config-default",
"transformers": {
"*.{js,mjs,jsx,cjs,ts,tsx}": [
"@parcel/transformer-js",
"@parcel/transformer-react-refresh-wrap"
]
}
}
5. ✅ Verify Installation
Run the test command to ensure everything is installed correctly. Initially, there will be no test cases.
Expected Output:
> [email protected] test
> jest
No tests found, exiting with code 1
6. 🛠️ Configure Jest
Initialize Jest configuration by running:
Follow the prompts:
Would you like to use Typescript for the configuration file? … no
Choose the test environment that will be used for testing › jsdom (browser-like)
Do you want Jest to add coverage reports? … yes
Which provider should be used to instrument code for coverage? › babel
Automatically clear mock calls, instances, contexts and results before every test? … yes
This creates a jest.config.js
file in your project.
Note
We are using jsdom as the test environment to simulate a browser-like environment for running tests.
7. 🌐 Install jsdom Environment
Info
For more information, refer to the Setup Testing Library with Jest Documentation.
Install jsdom:
8. 📝 Start Writing Test Cases
Example: Testing a Sum Function
- Create the function file:
- Create the test file:
// src/__tests__/sum.test.js
import { sum } from "../sum";
test("Function should calculate the sum of two numbers", () => {
const result = sum(3, 4);
expect(result).toBe(7);
});
- Run the test:
Expected Output:
> [email protected] test
> jest
PASS src/__tests__/sum.test.js
✓ Function should calculate the sum of two numbers
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
sum.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.306 s
Ran all test suites.
🧪 Testing React Components Made Easy
Testing ensures your React components work as expected. This guide covers Unit Testing and Integration Testing using Jest and React Testing Library. We'll use skeletal component structures for clarity and explain the reasoning behind each step, including important distinctions between various testing methods.
🧩 Unit Testing
Unit Testing focuses on individual components to ensure they function correctly in isolation.
📄 Example Component Structure
Instead of a complete component, we'll use a skeleton to focus on testing logic.
// Contact.jsx
const Contact = () => {
return (
<div>
<h1>Contact</h1>
<form>
<input type="text" placeholder="Your Name" />
<input type="email" placeholder="Your Email" />
<textarea placeholder="Your Message"></textarea>
<button>Send</button>
</form>
</div>
);
};
export default Contact;
✅ Writing a Test Case
Objective: Verify that the Contact
component renders correctly.
Why: Ensures that all essential elements are present, preventing rendering issues.
// Contact.test.jsx
import { render, screen } from '@testing-library/react';
import Contact from './Contact';
import '@testing-library/jest-dom'; // Extends Jest with custom matchers
test('renders Contact component', () => {
render(<Contact />);
const heading = screen.getByRole('heading', { name: /contact/i });
expect(heading).toBeInTheDocument();
});
🔍 Understanding @testing-library/jest-dom
- Purpose: Extends Jest with custom matchers like
toBeInTheDocument()
, enhancing test readability and expressiveness. - Usage: Import it once in your test files to use additional matchers.
Issue: Missing @testing-library/jest-dom
library can cause errors like TypeError: expect(...).toBeInTheDocument is not a function
.
Solution:
-
Install Jest DOM:
-
Import Jest DOM in Test File:
🔍 Why Use { name: /contact/i }
?
- Purpose: The
{ name: /contact/i }
option ingetByRole
searches for a heading with text matching the regular expression/contact/i
. - Reason:
/contact/i
: The/i
flag makes the search case-insensitive.- Enhances Flexibility: Allows the test to pass regardless of the text case, ensuring robustness.
🔧 Enabling JSX in Testing
Issue: JSX may not be enabled, causing rendering errors.
Solution: Configure Babel to support JSX during tests.
-
Install Babel Preset for React:
-
Configure Babel (
babel.config.json
):
📚 Grouping Multiple Test Cases
Why: Organizes related tests, making the test suite more readable and maintainable.
// Contact.test.jsx
import { render, screen } from '@testing-library/react';
import Contact from './Contact';
import '@testing-library/jest-dom';
describe('Contact Component', () => {
test('renders Contact heading', () => {
render(<Contact />);
const heading = screen.getByRole('heading', { name: /contact/i });
expect(heading).toBeInTheDocument();
});
test('renders Send button', () => {
render(<Contact />);
const button = screen.getByText(/send/i);
expect(button).toBeInTheDocument();
});
});
🔗 Integration Testing
Integration Testing evaluates how different parts of your application work together.
🛠 Automating Tests with a Watch Script
Why: Automatically run tests on file changes, improving development efficiency.
-
Add
watch-test
Script (package.json
): -
Run Watch Script:
📄 Example Component Structures
Header Component Skeleton:
// Header.jsx
import { Link } from 'react-router-dom';
import { useSelector } from 'react-redux';
const Header = () => {
const cartItems = useSelector((store) => store.cart.items);
return (
<div>
<h1>Site Name</h1>
<nav>
<Link to="/">Home</Link>
<Link to="/cart">Cart ({cartItems.length})</Link>
<button>Login</button>
</nav>
</div>
);
};
export default Header;
Body Component Skeleton:
// Body.jsx
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
const Body = () => {
const [restaurants, setRestaurants] = useState([]);
const [filteredRestaurants, setFilteredRestaurants] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
// Fetch restaurants from API
}, []);
return (
<div>
<input placeholder="Search Restaurants" />
<button>Search</button>
<button>Top Rated Restaurants</button>
{/* Restaurant cards will be rendered here */}
</div>
);
};
export default Body;
✅ Writing Test Cases for Header Component
🔧 Understanding Provider
and BrowserRouter
Provider
:- Purpose: Makes the Redux store available to any nested components that need to access the Redux store.
-
Usage: Wrap your component with
Provider
and pass the store as a prop. -
BrowserRouter
: - Purpose: Provides routing context to the components, enabling navigation functionalities.
- Usage: Wrap your component with
BrowserRouter
to enable routing features likeLink
andRoute
.
// Header.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Header from './Header';
import { Provider } from 'react-redux';
import store from '../../store';
import { BrowserRouter } from 'react-router-dom';
import '@testing-library/jest-dom'; // Extends Jest with custom matchers
test('renders Header with Login button', () => {
render(
<BrowserRouter>
<Provider store={store}>
<Header />
</Provider>
</BrowserRouter>
);
const loginButton = screen.getByText(/login/i);
expect(loginButton).toBeInTheDocument();
});
test('changes Login to Logout on button click', () => {
render(
<BrowserRouter>
<Provider store={store}>
<Header />
</Provider>
</BrowserRouter>
);
const loginButton = screen.getByRole('button', { name: /login/i });
fireEvent.click(loginButton);
const logoutButton = screen.getByRole('button', { name: /logout/i });
expect(logoutButton).toBeInTheDocument();
});
🔥 What is fireEvent
?
- Purpose: Simulates user interactions (e.g., clicks, typing) within tests.
- Usage:
- Syntax:
fireEvent.eventName(element, eventData)
- Example:
🔍 What is getByPlaceholderText
?
Purpose: Selects input elements based on their placeholder text.
Syntax:
Usage Example:
const searchInput = screen.getByPlaceholderText(/search restaurants/i);
fireEvent.change(searchInput, { target: { value: 'Pizza' } });
Why Use It:
- Clarity: Targets inputs by their descriptive placeholder.
- Accessibility: Reflects how users identify input fields through hints.
✅ Writing Test Cases for Body Component
Objective: Test search functionality and API integration in Body
component.
Why: Ensures that user interactions trigger the correct filtering and that API data is handled properly.
📁 Mocking Fetch API
Why: Prevents actual API calls during tests, ensuring consistency and speed.
-
Create Mock Data:
// __mocks__/resListData.json [ { "id": "1", "data": { "name": "Burger King", "rating": 4.5, "cuisins": ["American"], "time": "30", "promoted": true, "logo": "https://example.com/logo1.png" } }, { "id": "2", "data": { "name": "McDonald's", "rating": 4.2, "cuisins": ["Fast Food"], "time": "25", "promoted": false, "logo": "https://example.com/logo2.png" } } // Add more mock restaurant data as needed ]
-
Mock
fetch
Globally:// Body.test.jsx import { fireEvent, render, screen } from "@testing-library/react"; import Body from "../Body"; import REST_LIST_DATA from "../__mocks__/resListData.json"; import { act } from "react-dom/test-utils"; import '@testing-library/jest-dom'; import { Provider } from "react-redux"; import store from "../../store"; import { BrowserRouter } from "react-router-dom"; // Mock Fetch API globally global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve(REST_LIST_DATA), }) );
Why Define
global.fetch
:- Isolation: Ensures tests do not make real API calls, which can be slow and flaky.
- Control: Allows you to define exact responses, making tests predictable and repeatable.
- Performance: Speeds up tests by avoiding network latency.
⚙️ Creating a Wrapper for Rendering
Why: Provides necessary context (Redux store and Router) to the component being tested.
const BodyWrapper = () => (
<BrowserRouter>
<Provider store={store}>
<Body />
</Provider>
</BrowserRouter>
);
🔄 Writing Test Cases
describe("Body Component", () => {
beforeEach(() => {
fetch.mockClear(); // Clears previous mock calls
});
test("loads restaurant list after API call", async () => {
await act(async () => {
render(<BodyWrapper />);
});
const restaurantCards = await screen.findAllByTestId("resCard");
expect(restaurantCards.length).toBe(REST_LIST_DATA.length);
});
test("searches for a specific restaurant", async () => {
await act(async () => {
render(<BodyWrapper />);
});
const searchInput = screen.getByPlaceholderText(/search restaurants/i);
fireEvent.change(searchInput, { target: { value: "Burger" } });
const searchButton = screen.getByRole("button", { name: /search/i });
fireEvent.click(searchButton);
const searchResults = await screen.findAllByTestId("resCard");
const filtered = REST_LIST_DATA.filter(restaurant =>
restaurant.data.name.toLowerCase().includes("burger".toLowerCase())
);
expect(searchResults.length).toBe(filtered.length);
});
test("filters top-rated restaurants", async () => {
await act(async () => {
render(<BodyWrapper />);
});
const topRatedButton = screen.getByRole("button", { name: /top rated restaurants/i });
fireEvent.click(topRatedButton);
const filteredResults = await screen.findAllByTestId("resCard");
const expected = REST_LIST_DATA.filter(restaurant => restaurant.data.rating > 4);
expect(filteredResults.length).toBe(expected.length);
});
});
🔄 Why Use async
and await
with act
?
- Purpose of
act
: Ensures that all updates related to component rendering and state changes are processed before making assertions. - Why
async/await
: - Asynchronous Operations: Components often perform async tasks (e.g., API calls) that need to complete before tests proceed.
- Prevent Race Conditions: Using
async/await
withact
ensures that the component has fully rendered before assertions run.
📜 What is import { act } from "react-dom/test-utils"
?
- Purpose: Batches state updates and effects, ensuring that component rendering is completed before assertions.
- Usage: Wrap asynchronous operations that trigger state updates within
act
to simulate real user interactions accurately.
🟢 Using getByPlaceholderText
vs getAllByPlaceholderText
getByPlaceholderText
:- Use When: You need to select a single input element by its placeholder text.
-
Example:
-
getAllByPlaceholderText
: - Use When: You expect multiple input elements sharing the same placeholder text and want to select all of them.
-
Example:
-
Difference:
getByPlaceholderText
throws an error if no element or multiple elements match.getAllByPlaceholderText
returns an array of all matching elements and throws an error if none are found.
🟢 Using findByTestId
vs findAllByTestId
findByTestId
:- Use When: You need to asynchronously find a single element by its
data-testid
. - Returns: A Promise that resolves to the found element.
-
Example:
-
findAllByTestId
: - Use When: You need to asynchronously find all elements matching a
data-testid
. - Returns: A Promise that resolves to an array of found elements.
-
Example:
-
Difference:
findByTestId
is for a single element, whilefindAllByTestId
is for multiple elements.- Both are asynchronous and useful for elements that appear after certain actions or delays.
🛠 Helper Functions in Jest
Jest provides functions to run code at specific stages of the testing lifecycle:
beforeAll()
: Runs once before all tests in a suite.afterAll()
: Runs once after all tests in a suite.beforeEach()
: Runs before each test.afterEach()
: Runs after each test.
Example:
describe("Test Suite", () => {
beforeAll(() => {
// Setup before all tests
});
afterAll(() => {
// Cleanup after all tests
});
beforeEach(() => {
// Setup before each test
});
afterEach(() => {
// Cleanup after each test
});
test("test case 1", () => {
// Test implementation
});
test("test case 2", () => {
// Test implementation
});
});
Why Use Helper Functions:
- Code Reusability: Avoids repetition by setting up common prerequisites.
- Maintainability: Centralizes setup and teardown logic, making tests cleaner.
💡 Additional Testing Tips
🎯 Using getByRole
vs getAllByRole
getByRole
:- Use When: Selecting a single element by its ARIA role.
-
Example:
-
getAllByRole
: - Use When: Selecting multiple elements sharing the same ARIA role.
- Example:
Recommendation: Prefer getByRole
for better accessibility and specificity. Use getAllByRole
when multiple elements match the role.
🔄 Firing Events
Why: Simulates user interactions to test component responses.
Example:
Common Events:
- click
: Simulates a mouse click.
- change
: Simulates a value change in input elements.
- submit
: Simulates form submission.
🔁 Testing Components with Redux
Why: Ensures components interacting with the global state behave correctly.
How:
-
Wrap Component with
Provider
: -
Why:
- State Access: Components using Redux hooks (
useSelector
,useDispatch
) require access to the Redux store. - Consistency: Mimics the actual app environment where the store is provided.
- State Access: Components using Redux hooks (
Example Test:
test('renders Header with Cart count from Redux store', () => {
render(
<Provider store={store}>
<Header />
</Provider>
);
const cartLink = screen.getByText(/cart/i);
expect(cartLink).toHaveTextContent(`Cart (${store.getState().cart.items.length})`);
});
🔀 Testing Components with React Router
Why: Verifies navigation-related functionalities work as intended.
How:
-
Wrap Component with
BrowserRouter
: -
Why:
- Routing Context: Components using
Link
,Route
, or other router features need routing context to function. - Prevent Errors: Avoids errors related to missing router context during tests.
- Routing Context: Components using
Example Test:
test('navigates to home page on Home link click', () => {
render(
<BrowserRouter>
<Header />
</BrowserRouter>
);
const homeLink = screen.getByText(/home/i);
expect(homeLink).toHaveAttribute('href', '/');
});
🧩 Testing Components with Props
Why: Ensures components correctly handle and display received data.
Example:
// ItemCard.jsx
const ItemCard = ({ name, price }) => (
<div>
<h2>{name}</h2>
<p>{price}</p>
</div>
);
export default ItemCard;
Test Case:
// ItemCard.test.jsx
import { render, screen } from '@testing-library/react';
import ItemCard from './ItemCard';
test('renders ItemCard with props', () => {
const mockProps = { name: 'Sample Item', price: '$10' };
render(<ItemCard {...mockProps} />);
expect(screen.getByText('Sample Item')).toBeInTheDocument();
expect(screen.getByText('$10')).toBeInTheDocument();
});
🔍 Understanding findBy
and findAllBy
findBy
:- Use When: You need to asynchronously find a single element.
- Returns: A Promise that resolves to the found element.
-
Example:
-
findAllBy
: - Use When: You need to asynchronously find multiple elements.
- Returns: A Promise that resolves to an array of found elements.
- Example:
Key Points:
- Both are asynchronous and return Promises.
- Use them for elements that appear after asynchronous actions (e.g., API calls).
- Handle them with
async/await
in your tests.
📁 Git Configuration
🚫 Ignoring the Coverage Folder
After running tests, a coverage
folder is generated. To prevent cluttering your Git repository:
-
Add to
.gitignore
:
📜 Summary
-
Unit Testing:
- Focus: Individual components.
- Tools: Jest, React Testing Library.
- Practices: Use semantic selectors (
getByRole
), mock dependencies, group tests withdescribe
.
-
Integration Testing:
- Focus: How components work together.
- Tools: Jest, React Testing Library.
- Practices: Mock API calls, provide necessary context (Redux, Router), test user interactions.
-
Best Practices:
- Use Semantic Selectors: Enhances accessibility and test reliability (
getByRole
overgetByTestId
when possible). - Mock External Services: Isolate tests and ensure consistency (
global.fetch
mocking). - Organize Tests: Group related tests using
describe
for clarity. - Utilize Helper Functions: Streamline setup and teardown processes (
beforeEach
,afterEach
). - Handle Asynchronous Code Properly: Use
act
,async/await
to manage state updates and async operations. - Keep Tests Focused: Ensure each test checks a specific functionality for better maintainability.
- Use Semantic Selectors: Enhances accessibility and test reliability (