Learn React testing with Jest and React Testing Library, and deployment strategies for React applications.
Learn React testing with Jest and React Testing Library, and deployment strategies for React applications.
Learn Jest testing framework for testing React components and JavaScript code
Content by: Sagar Dholariya
MERN Stack Developer
Jest is a JavaScript testing framework designed to ensure correctness of any JavaScript codebase. It allows you to write tests with an approachable, familiar and feature-rich API that gives you results quickly.
// Basic Jest test
function sum(a, b) {
return a + b;
}
// Test file: sum.test.js
describe('sum function', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
test('adds negative numbers correctly', () => {
expect(sum(-1, -2)).toBe(-3);
});
test('handles zero correctly', () => {
expect(sum(0, 5)).toBe(5);
});
});
// Testing async functions
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
describe('fetchUser', () => {
test('fetches user successfully', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
});
test('handles errors correctly', async () => {
await expect(fetchUser(999)).rejects.toThrow();
});
});
// Mocking functions
const mockFetch = jest.fn();
beforeEach(() => {
mockFetch.mockClear();
});
test('mocks fetch correctly', async () => {
mockFetch.mockResolvedValueOnce({
json: async () => ({ id: 1, name: 'John' })
});
const result = await fetchUser(1);
expect(result).toEqual({ id: 1, name: 'John' });
expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
});
// Testing utility functions
// src/utils/validation.js
export function validateEmail(email) {
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
return emailRegex.test(email);
}
export function validatePassword(password) {
return password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password);
}
// src/utils/validation.test.js
import { validateEmail, validatePassword } from './validation';
describe('validateEmail', () => {
test('validates correct email formats', () => {
expect(validateEmail('test@example.com')).toBe(true);
expect(validateEmail('user.name@domain.co.uk')).toBe(true);
expect(validateEmail('user+tag@example.org')).toBe(true);
});
test('rejects invalid email formats', () => {
expect(validateEmail('invalid-email')).toBe(false);
expect(validateEmail('test@')).toBe(false);
expect(validateEmail('@example.com')).toBe(false);
expect(validateEmail('')).toBe(false);
});
});
describe('validatePassword', () => {
test('validates strong passwords', () => {
expect(validatePassword('StrongPass123')).toBe(true);
expect(validatePassword('MySecure1')).toBe(true);
});
test('rejects weak passwords', () => {
expect(validatePassword('weak')).toBe(false);
expect(validatePassword('12345678')).toBe(false);
expect(validatePassword('onlylowercase')).toBe(false);
expect(validatePassword('ONLYUPPERCASE')).toBe(false);
});
});
// Testing API utilities
// src/utils/api.js
export class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async get(endpoint) {
const response = await fetch(`${this.baseURL}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async post(endpoint, data) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
}
// src/utils/api.test.js
import { ApiClient } from './api';
describe('ApiClient', () => {
let apiClient;
let mockFetch;
beforeEach(() => {
apiClient = new ApiClient('https://api.example.com');
mockFetch = jest.fn();
global.fetch = mockFetch;
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('get', () => {
test('successfully fetches data', async () => {
const mockData = { id: 1, name: 'John' };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData,
});
const result = await apiClient.get('/users/1');
expect(result).toEqual(mockData);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users/1');
});
test('throws error on failed request', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
});
await expect(apiClient.get('/users/999')).rejects.toThrow('HTTP error! status: 404');
});
});
describe('post', () => {
test('successfully posts data', async () => {
const postData = { name: 'John', email: 'john@example.com' };
const mockResponse = { id: 1, ...postData };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
const result = await apiClient.post('/users', postData);
expect(result).toEqual(mockResponse);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
});
});
});
});
// Exercise: Build a Complete Testing Suite for a Todo App
// Create comprehensive tests for a React Todo application
// src/components/TodoApp.js
import React, { useState, useEffect } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const [filter, setFilter] = useState('all');
useEffect(() => {
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
setTodos(JSON.parse(savedTodos));
}
}, []);
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = () => {
if (newTodo.trim()) {
setTodos(prev => [...prev, {
id: Date.now(),
text: newTodo,
completed: false,
createdAt: new Date().toISOString()
}]);
setNewTodo('');
}
};
const toggleTodo = (id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<div className="todo-app">
<h1>Todo App</h1>
<div className="add-todo">
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add new todo..."
data-testid="new-todo-input"
/>
<button onClick={addTodo} data-testid="add-todo-button">
Add Todo
</button>
</div>
<div className="filters">
<button
onClick={() => setFilter('all')}
className={filter === 'all' ? 'active' : ''}
data-testid="filter-all"
>
All
</button>
<button
onClick={() => setFilter('active')}
className={filter === 'active' ? 'active' : ''}
data-testid="filter-active"
>
Active
</button>
<button
onClick={() => setFilter('completed')}
className={filter === 'completed' ? 'active' : ''}
data-testid="filter-completed"
>
Completed
</button>
</div>
<ul className="todo-list" data-testid="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
data-testid={`todo-checkbox-${todo.id}`}
/>
<span
className={todo.completed ? 'completed' : ''}
data-testid={`todo-text-${todo.id}`}
>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
data-testid={`delete-todo-${todo.id}`}
>
Delete
</button>
</li>
))}
</ul>
<div className="todo-stats">
<span data-testid="todo-count">
{todos.filter(todo => !todo.completed).length} items left
</span>
</div>
</div>
);
}
// src/components/TodoApp.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import TodoApp from './TodoApp';
// Mock localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;
describe('TodoApp', () => {
beforeEach(() => {
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockClear();
});
test('renders todo app with title', () => {
render(<TodoApp />);
expect(screen.getByText('Todo App')).toBeInTheDocument();
});
test('adds new todo when form is submitted', () => {
render(<TodoApp />);
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Test todo' } });
fireEvent.click(addButton);
expect(screen.getByText('Test todo')).toBeInTheDocument();
expect(input).toHaveValue('');
});
test('does not add empty todo', () => {
render(<TodoApp />);
const addButton = screen.getByTestId('add-todo-button');
fireEvent.click(addButton);
const todoList = screen.getByTestId('todo-list');
expect(todoList.children).toHaveLength(0);
});
test('toggles todo completion', () => {
render(<TodoApp />);
// Add a todo first
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Test todo' } });
fireEvent.click(addButton);
const checkbox = screen.getByTestId('todo-checkbox-1');
const todoText = screen.getByTestId('todo-text-1');
expect(checkbox).not.toBeChecked();
expect(todoText).not.toHaveClass('completed');
fireEvent.click(checkbox);
expect(checkbox).toBeChecked();
expect(todoText).toHaveClass('completed');
});
test('deletes todo when delete button is clicked', () => {
render(<TodoApp />);
// Add a todo first
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Test todo' } });
fireEvent.click(addButton);
expect(screen.getByText('Test todo')).toBeInTheDocument();
const deleteButton = screen.getByTestId('delete-todo-1');
fireEvent.click(deleteButton);
expect(screen.queryByText('Test todo')).not.toBeInTheDocument();
});
test('filters todos correctly', () => {
render(<TodoApp />);
// Add multiple todos
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Todo 1' } });
fireEvent.click(addButton);
fireEvent.change(input, { target: { value: 'Todo 2' } });
fireEvent.click(addButton);
// Complete first todo
const checkbox1 = screen.getByTestId('todo-checkbox-1');
fireEvent.click(checkbox1);
// Test active filter
const activeFilter = screen.getByTestId('filter-active');
fireEvent.click(activeFilter);
expect(screen.getByText('Todo 2')).toBeInTheDocument();
expect(screen.queryByText('Todo 1')).not.toBeInTheDocument();
// Test completed filter
const completedFilter = screen.getByTestId('filter-completed');
fireEvent.click(completedFilter);
expect(screen.getByText('Todo 1')).toBeInTheDocument();
expect(screen.queryByText('Todo 2')).not.toBeInTheDocument();
});
test('loads todos from localStorage on mount', () => {
const savedTodos = [
{ id: 1, text: 'Saved todo', completed: false, createdAt: '2025-01-01' }
];
localStorageMock.getItem.mockReturnValue(JSON.stringify(savedTodos));
render(<TodoApp />);
expect(screen.getByText('Saved todo')).toBeInTheDocument();
expect(localStorageMock.getItem).toHaveBeenCalledWith('todos');
});
test('saves todos to localStorage when todos change', async () => {
render(<TodoApp />);
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Test todo' } });
fireEvent.click(addButton);
await waitFor(() => {
expect(localStorageMock.setItem).toHaveBeenCalled();
});
});
test('displays correct todo count', () => {
render(<TodoApp />);
// Add two todos
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Todo 1' } });
fireEvent.click(addButton);
fireEvent.change(input, { target: { value: 'Todo 2' } });
fireEvent.click(addButton);
expect(screen.getByTestId('todo-count')).toHaveTextContent('2 items left');
// Complete one todo
const checkbox = screen.getByTestId('todo-checkbox-1');
fireEvent.click(checkbox);
expect(screen.getByTestId('todo-count')).toHaveTextContent('1 items left');
});
});
// Challenge: Add integration tests with a real API
// Challenge: Add performance tests for large todo lists
// Challenge: Add accessibility tests
Test your understanding of this topic:
Master React Testing Library for testing React components from a user's perspective
Content by: Harmi Savani
MERN Stack Developer
React Testing Library is a very light-weight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils, in a way that encourages better testing practices.
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
// Component to test
function Counter() {
const [count, setCount] = React.useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
}
// Test file: Counter.test.js
describe('Counter', () => {
test('renders counter with initial value', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
expect(screen.getByText('Increment')).toBeInTheDocument();
expect(screen.getByText('Decrement')).toBeInTheDocument();
});
test('increments counter when increment button is clicked', () => {
render(<Counter />);
const incrementButton = screen.getByText('Increment');
fireEvent.click(incrementButton);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('decrements counter when decrement button is clicked', () => {
render(<Counter />);
const decrementButton = screen.getByText('Decrement');
fireEvent.click(decrementButton);
expect(screen.getByText('Count: -1')).toBeInTheDocument();
});
});
// Testing forms
function LoginForm({ onSubmit }) {
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
data-testid="email-input"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
data-testid="password-input"
/>
<button type="submit">Login</button>
</form>
);
}
describe('LoginForm', () => {
test('submits form with correct values', () => {
const mockOnSubmit = jest.fn();
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByTestId('email-input');
const passwordInput = screen.getByTestId('password-input');
const submitButton = screen.getByText('Login');
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);
expect(mockOnSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
});
// Testing async operations
import { render, screen, waitFor } from '@testing-library/react';
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Test with mocked fetch
describe('UserProfile', () => {
beforeEach(() => {
fetch.resetMocks();
});
test('renders user data after successful fetch', async () => {
const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com' };
fetch.mockResponseOnce(JSON.stringify(mockUser));
render(<UserProfile userId={1} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
test('renders error message on fetch failure', async () => {
fetch.mockRejectOnce(new Error('Failed to fetch'));
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('Error: Failed to fetch')).toBeInTheDocument();
});
});
});
Test your understanding of this topic:
Learn different deployment strategies and platforms for React applications
Content by: Abhay Khanpara
MERN Stack Developer
Before deploying a React application, you need to build it for production. This process optimizes the code, minifies it, and creates static files that can be served by a web server.
// package.json scripts
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
// Build for production
npm run build
// This creates a 'build' folder with optimized files:
// - index.html
// - static/js/
// - static/css/
// - static/media/
// Environment variables
// .env.production
REACT_APP_API_URL=https://api.production.com
REACT_APP_ENVIRONMENT=production
// .env.development
REACT_APP_API_URL=http://localhost:3001
REACT_APP_ENVIRONMENT=development
// Using environment variables in code
const apiUrl = process.env.REACT_APP_API_URL;
const environment = process.env.REACT_APP_ENVIRONMENT;
// Netlify deployment
// netlify.toml
[build]
command = "npm run build"
publish = "build"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
// Vercel deployment
// vercel.json
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": { "distDir": "build" }
}
],
"routes": [
{
"src": "/static/(.*)",
"dest": "/static/$1"
},
{
"src": "/favicon.ico",
"dest": "/favicon.ico"
},
{
"src": "/manifest.json",
"dest": "/manifest.json"
},
{
"src": "/(.*)",
"dest": "/index.html"
}
]
}
// GitHub Pages deployment
// package.json
{
"homepage": "https://username.github.io/repository-name",
"scripts": {
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
}
}
// Deploy to GitHub Pages
npm run deploy
Test your understanding of this topic:
Learn to set up continuous integration and deployment pipelines for React applications
Content by: Nitin Parmar
MERN Stack Developer
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --coverage --watchAll=false
- name: Run linting
run: npm run lint
- name: Build application
run: npm run build
- name: Upload coverage reports
uses: codecov/codecov-action@v3
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v2
with:
publish-dir: './build'
production-branch: main
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
# Dockerfile
FROM node:18-alpine as build
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built files to nginx
COPY --from=build /app/build /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Handle React Router
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* .(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# docker-compose.yml
version: '3.8'
services:
react-app:
build: .
ports:
- "80:80"
environment:
- NODE_ENV=production
Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Back to Course Overview