Module 6: Testing and Deployment

Learn React testing with Jest and React Testing Library, and deployment strategies for React applications.

Back to Course|5.5 hours|Advanced

Testing and Deployment

Learn React testing with Jest and React Testing Library, and deployment strategies for React applications.

Progress: 0/4 topics completed0%

Select Topics Overview

Jest Testing

Learn Jest testing framework for testing React components and JavaScript code

Content by: Sagar Dholariya

MERN Stack Developer

Connect

What is Jest?

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 Tests

Code Example
// 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),
            });
        });
    });
});
Swipe to see more code

Practice Exercise: Complete Testing Suite

Code Example
// 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
Swipe to see more code

๐ŸŽฏ Practice Exercise

Test your understanding of this topic:

Additional Resources

๐Ÿ“š Recommended Reading

  • โ€ขJest Documentation
  • โ€ขReact Testing Library Documentation
  • โ€ขDeployment Best Practices

๐ŸŒ Online Resources

  • โ€ขJest Testing Tutorial
  • โ€ขReact Testing Library Guide
  • โ€ขCI/CD Pipeline Examples

Ready for the Next Module?

Continue your learning journey and master the next set of concepts.

Back to Course Overview