Automated testing is the practice of writing code to test your code. It's a safety net that helps you catch bugs early, refactor with confidence, and build more reliable applications. Let's look at two fundamental levels of testing.
Unit Testing with Jest
A unit test verifies the smallest piece of testable code in isolation, which is usually a single function. It checks: "Given a certain input, does this function return the expected output?"
Jest is a popular JavaScript testing framework that provides everything you need to write unit tests: a test runner, assertion libraries, and mocking capabilities.
Let's say we have a simple sum function.
JavaScript
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
Here's how you'd write a Jest test for it. Test files usually end with .test.js or .spec.js.
JavaScript
// sum.test.js
const sum = require('./sum');
// The 'test' function defines a single test case.
// First argument: A string describing the test.
// Second argument: A function containing the test logic.
test('adds 1 + 2 to equal 3', () => {
// 'expect' is the assertion. It's chained with a "matcher" function.
// 'toBe' is a matcher that checks for exact equality.
expect(sum(1, 2)).toBe(3);
});
test('adds -5 + 10 to equal 5', () => {
expect(sum(-5, 10)).toBe(5);
});
To run the tests, you'd execute the jest command in your terminal. Jest will find all test files and report if they pass or fail.
Component Testing with React Testing Library (RTL)
While unit tests are great for business logic, they don't tell you if your UI works. Component testing focuses on verifying that your UI components render correctly and behave as expected from a user's perspective.
React Testing Library (RTL) is a testing utility for React that encourages good testing practices. Its guiding principle is: "The more your tests resemble the way your software is used, the more confidence they can give you."
Instead of testing implementation details (like a component's internal state), RTL tests how a user would interact with the component.
Let's imagine a simple <Counter> component.
JavaScript
// Counter.js
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;
Here's an RTL test for it, often used with Jest as the test runner.
JavaScript
// Counter.test.js
import React from 'react';
// 'render' renders the component, 'screen' is an object to query the DOM.
// 'fireEvent' simulates user events.
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; // Adds custom matchers like .toBeInTheDocument()
import Counter from './Counter';
test('renders initial count and increments on button click', () => {
// 1. Render the component
render(<Counter />);
// 2. Find elements on the screen the way a user would
// We find the button by its accessible name (the text content)
const button = screen.getByRole('button', { name: /increment/i });
const countText = screen.getByText(/count: 0/i);
// 3. Assert that the initial state is correct
expect(countText).toBeInTheDocument();
expect(button).toBeInTheDocument();
// 4. Simulate a user action
fireEvent.click(button);
// 5. Assert the result of the action
// The count text should now be updated
const updatedCountText = screen.getByText(/count: 1/i);
expect(updatedCountText).toBeInTheDocument();
});
This test doesn't know about useState. It just knows that when you click a button named "Increment," the text on the screen changes from "Count: 0" to "Count: 1," just like a real user would see.