Testing is a critical part of frontend development that ensures your applications work correctly, remain maintainable, and provide a good user experience. Effective testing:
Catches bugs early in the development process
Provides confidence when refactoring or adding new features
Serves as living documentation for how your code should behave
Improves code quality and architecture
Reduces the cost of fixing issues in production
While testing requires an upfront investment of time, it saves significant time and resources in the long run by preventing regressions and making your codebase more maintainable.
Include tests for boundary conditions and error scenarios.
Copy
// Function to testfunction divideNumbers(a, b) { if (b === 0) { throw new Error('Cannot divide by zero'); } return a / b;}// Unit tests including edge casesdescribe('divideNumbers', () => { test('divides two positive numbers', () => { expect(divideNumbers(10, 2)).toBe(5); }); test('divides a positive and negative number', () => { expect(divideNumbers(10, -2)).toBe(-5); }); test('divides zero by a number', () => { expect(divideNumbers(0, 5)).toBe(0); }); test('throws error when dividing by zero', () => { expect(() => divideNumbers(10, 0)).toThrow('Cannot divide by zero'); });});
Focus on how components communicate and work together.
Copy
// Integration test for a form and its submission handlerimport { render, screen, fireEvent, waitFor } from '@testing-library/react';import UserForm from './UserForm';import UserService from './UserService';// Mock the service module but not its implementationjest.mock('./UserService');test('submitting the form calls the create user service', async () => { // Arrange UserService.createUser = jest.fn().mockResolvedValue({ id: 1 }); render(<UserForm />); // Act - Fill out the form fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'John Doe' } }); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'john@example.com' } }); // Submit the form fireEvent.click(screen.getByRole('button', { name: /submit/i })); // Assert await waitFor(() => { expect(UserService.createUser).toHaveBeenCalledWith({ name: 'John Doe', email: 'john@example.com' }); }); expect(screen.getByText(/user created successfully/i)).toBeInTheDocument();});
Minimize mocking
Use real implementations where possible, only mock external dependencies.
Copy
// Integration test with minimal mockingimport { render, screen, fireEvent } from '@testing-library/react';import { BrowserRouter } from 'react-router-dom';import ShoppingCart from './ShoppingCart';import CartContext from './CartContext';// Only mock the API call, not the context or componentsjest.mock('./api', () => ({ checkout: jest.fn().mockResolvedValue({ orderId: '12345' })}));test('checkout process works end-to-end', async () => { // Arrange - Use real context with test data const cartItems = [ { id: 1, name: 'Product 1', price: 10, quantity: 2 }, { id: 2, name: 'Product 2', price: 15, quantity: 1 } ]; render( <BrowserRouter> <CartContext.Provider value={{ items: cartItems, total: 35 }}> <ShoppingCart /> </CartContext.Provider> </BrowserRouter> ); // Act expect(screen.getByText('Total: $35')).toBeInTheDocument(); expect(screen.getByText('Product 1')).toBeInTheDocument(); expect(screen.getByText('Product 2')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /checkout/i })); // Assert await waitFor(() => { expect(screen.getByText(/order confirmed/i)).toBeInTheDocument(); expect(screen.getByText(/order id: 12345/i)).toBeInTheDocument(); });});
Test user flows
Focus on common paths users take through your application.
Copy
// Integration test for a user flowimport { render, screen, fireEvent, waitFor } from '@testing-library/react';import { BrowserRouter } from 'react-router-dom';import App from './App';import * as api from './api';jest.mock('./api');test('user can log in and view their profile', async () => { // Arrange api.login = jest.fn().mockResolvedValue({ token: 'fake-token', user: { id: 1, name: 'John' } }); api.getUserProfile = jest.fn().mockResolvedValue({ id: 1, name: 'John', email: 'john@example.com' }); render( <BrowserRouter> <App /> </BrowserRouter> ); // Act - Log in fireEvent.change(screen.getByLabelText(/username/i), { target: { value: 'john' } }); fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'password123' } }); fireEvent.click(screen.getByRole('button', { name: /log in/i })); // Wait for login to complete await waitFor(() => { expect(screen.getByText(/welcome, john/i)).toBeInTheDocument(); }); // Navigate to profile fireEvent.click(screen.getByRole('link', { name: /profile/i })); // Assert profile page shows correct data await waitFor(() => { expect(screen.getByText(/john@example.com/i)).toBeInTheDocument(); });});
Test data flow
Verify that data flows correctly between components and services.
Copy
// Integration test for data flowimport { render, screen, fireEvent, waitFor } from '@testing-library/react';import { QueryClient, QueryClientProvider } from 'react-query';import ProductList from './ProductList';import ProductDetail from './ProductDetail';import * as api from './api';jest.mock('./api');test('selecting a product shows its details', async () => { // Arrange const products = [ { id: 1, name: 'Product 1', price: 10 }, { id: 2, name: 'Product 2', price: 20 } ]; const productDetails = { id: 1, name: 'Product 1', price: 10, description: 'Detailed description', features: ['Feature 1', 'Feature 2'] }; api.getProducts = jest.fn().mockResolvedValue(products); api.getProductDetails = jest.fn().mockResolvedValue(productDetails); const queryClient = new QueryClient(); render( <QueryClientProvider client={queryClient}> <div> <ProductList /> <ProductDetail /> </div> </QueryClientProvider> ); // Wait for product list to load await waitFor(() => { expect(screen.getByText('Product 1')).toBeInTheDocument(); }); // Act - Select a product fireEvent.click(screen.getByText('Product 1')); // Assert - Product details are displayed await waitFor(() => { expect(screen.getByText('Detailed description')).toBeInTheDocument(); expect(screen.getByText('Feature 1')).toBeInTheDocument(); expect(screen.getByText('Feature 2')).toBeInTheDocument(); }); expect(api.getProductDetails).toHaveBeenCalledWith(1);});
Prioritize testing the most important user journeys:
Authentication flows
Core business processes
Payment or checkout flows
Data entry and submission
2
Use stable selectors
Avoid brittle selectors that break with UI changes:
Copy
// Bad - brittle selectorscy.get('div > p > span').click();cy.get('.menu-item:nth-child(3)').click();// Good - stable selectorscy.get('[data-testid="submit-button"]').click();cy.contains('Submit').click();
Add dedicated test attributes when needed:
Copy
<button data-testid="checkout-button">Proceed to Checkout</button>
// Cypress example with custom command// In cypress/support/commands.jsCypress.Commands.add('seedDatabase', (fixture) => { cy.task('db:seed', { fixture });});// In testcy.seedDatabase('products');cy.visit('/products');
describe('Form component', () => { describe('validation', () => { test('shows error when email is invalid', () => { // Test code }); test('shows error when password is too short', () => { // Test code }); }); describe('submission', () => { test('calls onSubmit with form data when valid', () => { // Test code }); test('shows loading state while submitting', () => { // Test code }); });});
Test for regressions
When fixing a bug, write a test that would have caught it.
Copy
// Bug: The search function was case-sensitive when it shouldn't be// Write a regression testtest('search is case-insensitive', () => { const items = [ { id: 1, name: 'Apple' }, { id: 2, name: 'Banana' }, { id: 3, name: 'Orange' } ]; const results = search(items, 'apple'); expect(results).toHaveLength(1); expect(results[0].id).toBe(1);});// Fix the bugfunction search(items, query) { if (!query) return items; const lowerCaseQuery = query.toLowerCase(); return items.filter(item => item.name.toLowerCase().includes(lowerCaseQuery) );}