← Back to Guides

Automated Testing Complete Guide

πŸ“– 17 min read | πŸ“… Updated: January 2025 | 🏷️ Other Technologies

Introduction

Automated testing ensures code quality and prevents regressions. This guide covers unit testing with Jest, integration testing, E2E testing with Cypress and Playwright, test strategies, mocking, and CI/CD integration.

1. Unit Testing with Jest

// Installation
npm install --save-dev jest @types/jest

// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

// Basic test
// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = { add, subtract };

// math.test.js
const { add, subtract } = require('./math');

describe('Math functions', () => {
  test('adds two numbers', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
  });
  
  test('subtracts two numbers', () => {
    expect(subtract(5, 3)).toBe(2);
  });
});

// Testing async code
// api.js
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// api.test.js
test('fetches user data', async () => {
  const user = await fetchUser(1);
  expect(user).toHaveProperty('id');
  expect(user).toHaveProperty('name');
});

// Or use resolves
test('fetches user data', () => {
  return expect(fetchUser(1)).resolves.toHaveProperty('id');
});

// Testing promises
test('promise resolves', () => {
  return expect(Promise.resolve('success')).resolves.toBe('success');
});

test('promise rejects', () => {
  return expect(Promise.reject('error')).rejects.toBe('error');
});

// Matchers
test('common matchers', () => {
  expect(2 + 2).toBe(4);                    // Exact equality
  expect({ name: 'John' }).toEqual({ name: 'John' });  // Deep equality
  expect(4).toBeGreaterThan(3);
  expect(4).toBeLessThanOrEqual(4);
  expect('team').toMatch(/tea/);
  expect(['apple', 'banana']).toContain('apple');
  expect(null).toBeNull();
  expect(undefined).toBeUndefined();
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
});

// Setup and teardown
describe('Database tests', () => {
  beforeAll(async () => {
    // Runs once before all tests
    await db.connect();
  });
  
  afterAll(async () => {
    // Runs once after all tests
    await db.disconnect();
  });
  
  beforeEach(async () => {
    // Runs before each test
    await db.clear();
  });
  
  afterEach(() => {
    // Runs after each test
    jest.clearAllMocks();
  });
  
  test('creates user', async () => {
    const user = await db.createUser({ name: 'John' });
    expect(user.id).toBeDefined();
  });
});

2. Mocking & Spies

// Mock functions
const mockFn = jest.fn();

mockFn('hello');
mockFn('world');

expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('hello');
expect(mockFn).toHaveBeenLastCalledWith('world');

// Mock return values
const mockCalculate = jest.fn();
mockCalculate.mockReturnValue(42);
mockCalculate.mockReturnValueOnce(10).mockReturnValueOnce(20);

expect(mockCalculate()).toBe(10);
expect(mockCalculate()).toBe(20);
expect(mockCalculate()).toBe(42);

// Mock implementations
const mockFilter = jest.fn((arr, predicate) => arr.filter(predicate));
const result = mockFilter([1, 2, 3, 4], x => x > 2);
expect(result).toEqual([3, 4]);

// Spy on methods
// user.js
class User {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, ${this.name}`;
  }
}

// user.test.js
test('spy on greet method', () => {
  const user = new User('John');
  const spy = jest.spyOn(user, 'greet');
  
  user.greet();
  
  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveReturnedWith('Hello, John');
  
  spy.mockRestore();  // Restore original implementation
});

// Mock modules
// emailService.js
async function sendEmail(to, subject, body) {
  // Real email sending logic
}

// user.test.js
jest.mock('./emailService');
const emailService = require('./emailService');

test('sends welcome email', async () => {
  emailService.sendEmail.mockResolvedValue({ success: true });
  
  const result = await emailService.sendEmail('test@example.com', 'Welcome', 'Hello!');
  
  expect(emailService.sendEmail).toHaveBeenCalledWith(
    'test@example.com',
    'Welcome',
    'Hello!'
  );
  expect(result).toEqual({ success: true });
});

// Partial mocks
jest.mock('./api', () => ({
  ...jest.requireActual('./api'),
  fetchUser: jest.fn()
}));

// Mock timers
test('delays execution', () => {
  jest.useFakeTimers();
  
  const callback = jest.fn();
  setTimeout(callback, 1000);
  
  expect(callback).not.toHaveBeenCalled();
  
  jest.advanceTimersByTime(1000);
  expect(callback).toHaveBeenCalled();
  
  jest.useRealTimers();
});

// Mock dates
test('uses current date', () => {
  jest.useFakeTimers().setSystemTime(new Date('2024-01-01'));
  
  const now = new Date();
  expect(now.getFullYear()).toBe(2024);
  
  jest.useRealTimers();
});

3. React Component Testing

// Install testing library
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event

// Button.jsx
function Button({ onClick, children, disabled = false }) {
  return (
    
  );
}

// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';

test('renders button with text', () => {
  render();
  expect(screen.getByText('Click me')).toBeInTheDocument();
});

test('calls onClick when clicked', async () => {
  const handleClick = jest.fn();
  render();
  
  await userEvent.click(screen.getByText('Click me'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

test('is disabled when disabled prop is true', () => {
  render();
  expect(screen.getByText('Click me')).toBeDisabled();
});

// Counter component
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    

Count: {count}

); } // Counter.test.jsx test('increments count', async () => { render(); expect(screen.getByText('Count: 0')).toBeInTheDocument(); await userEvent.click(screen.getByText('Increment')); expect(screen.getByText('Count: 1')).toBeInTheDocument(); await userEvent.click(screen.getByText('Increment')); expect(screen.getByText('Count: 2')).toBeInTheDocument(); }); // Form testing function LoginForm({ onSubmit }) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleSubmit = (e) => { e.preventDefault(); onSubmit({ email, password }); }; return (
setEmail(e.target.value)} placeholder="Email" /> setPassword(e.target.value)} placeholder="Password" />
); } test('submits form with credentials', async () => { const handleSubmit = jest.fn(); render(); await userEvent.type(screen.getByPlaceholderText('Email'), 'test@example.com'); await userEvent.type(screen.getByPlaceholderText('Password'), 'password123'); await userEvent.click(screen.getByText('Login')); expect(handleSubmit).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password123' }); }); // Testing async components function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => { setUser(data); setLoading(false); }); }, [userId]); if (loading) return
Loading...
; if (!user) return
User not found
; return
Name: {user.name}
; } test('loads and displays user', async () => { global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ id: 1, name: 'John' }) }) ); render(); expect(screen.getByText('Loading...')).toBeInTheDocument(); const userName = await screen.findByText('Name: John'); expect(userName).toBeInTheDocument(); expect(fetch).toHaveBeenCalledWith('/api/users/1'); });

4. Integration Testing

// API integration tests
const request = require('supertest');
const app = require('./app');
const db = require('./db');

describe('User API', () => {
  beforeAll(async () => {
    await db.connect();
  });
  
  afterAll(async () => {
    await db.disconnect();
  });
  
  beforeEach(async () => {
    await db.clear();
  });
  
  test('POST /api/users creates user', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'John', email: 'john@example.com' })
      .expect(201);
    
    expect(response.body).toHaveProperty('id');
    expect(response.body.name).toBe('John');
    
    // Verify in database
    const user = await db.users.findOne({ email: 'john@example.com' });
    expect(user).toBeTruthy();
  });
  
  test('GET /api/users/:id returns user', async () => {
    const user = await db.users.create({ name: 'Jane', email: 'jane@example.com' });
    
    const response = await request(app)
      .get(`/api/users/${user.id}`)
      .expect(200);
    
    expect(response.body.name).toBe('Jane');
  });
  
  test('GET /api/users/:id returns 404 for non-existent user', async () => {
    await request(app)
      .get('/api/users/999999')
      .expect(404);
  });
  
  test('authenticated endpoint requires token', async () => {
    await request(app)
      .get('/api/profile')
      .expect(401);
    
    const token = 'valid_jwt_token';
    await request(app)
      .get('/api/profile')
      .set('Authorization', `Bearer ${token}`)
      .expect(200);
  });
});

// Database integration tests
describe('User model', () => {
  test('creates user with hashed password', async () => {
    const user = await User.create({
      email: 'test@example.com',
      password: 'password123'
    });
    
    expect(user.password).not.toBe('password123');
    expect(await user.comparePassword('password123')).toBe(true);
  });
  
  test('enforces unique email constraint', async () => {
    await User.create({ email: 'test@example.com', password: 'pass1' });
    
    await expect(
      User.create({ email: 'test@example.com', password: 'pass2' })
    ).rejects.toThrow();
  });
});

// Testing with test database
// jest.config.js
module.exports = {
  testEnvironment: 'node',
  setupFilesAfterEnv: ['./tests/setup.js']
};

// tests/setup.js
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/testdb';
process.env.NODE_ENV = 'test';

5. E2E Testing with Cypress

// Installation
npm install --save-dev cypress

// package.json
{
  "scripts": {
    "cypress:open": "cypress open",
    "cypress:run": "cypress run"
  }
}

// cypress/e2e/login.cy.js
describe('Login flow', () => {
  beforeEach(() => {
    cy.visit('http://localhost:3000');
  });
  
  it('logs in successfully', () => {
    cy.get('input[name="email"]').type('user@example.com');
    cy.get('input[name="password"]').type('password123');
    cy.get('button[type="submit"]').click();
    
    cy.url().should('include', '/dashboard');
    cy.contains('Welcome back').should('be.visible');
  });
  
  it('shows error for invalid credentials', () => {
    cy.get('input[name="email"]').type('wrong@example.com');
    cy.get('input[name="password"]').type('wrongpass');
    cy.get('button[type="submit"]').click();
    
    cy.contains('Invalid credentials').should('be.visible');
  });
});

// Custom commands
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login');
  cy.get('input[name="email"]').type(email);
  cy.get('input[name="password"]').type(password);
  cy.get('button[type="submit"]').click();
});

// Use custom command
cy.login('user@example.com', 'password123');

// API intercepts
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');

// Mock API responses
cy.intercept('POST', '/api/orders', {
  statusCode: 201,
  body: { id: 1, total: 99.99 }
}).as('createOrder');

cy.get('button.checkout').click();
cy.wait('@createOrder');

// File uploads
cy.get('input[type="file"]').selectFile('cypress/fixtures/image.png');

// Testing forms
describe('Contact form', () => {
  it('submits form successfully', () => {
    cy.visit('/contact');
    
    cy.get('#name').type('John Doe');
    cy.get('#email').type('john@example.com');
    cy.get('#message').type('Hello, this is a test message');
    
    cy.get('button[type="submit"]').click();
    
    cy.contains('Message sent successfully').should('be.visible');
  });
  
  it('validates required fields', () => {
    cy.visit('/contact');
    cy.get('button[type="submit"]').click();
    
    cy.contains('Name is required').should('be.visible');
    cy.contains('Email is required').should('be.visible');
  });
});

6. E2E Testing with Playwright

// Installation
npm install --save-dev @playwright/test

// playwright.config.js
module.exports = {
  testDir: './tests',
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
};

// tests/login.spec.js
const { test, expect } = require('@playwright/test');

test('user can log in', async ({ page }) => {
  await page.goto('/login');
  
  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
  
  await expect(page).toHaveURL(/.*dashboard/);
  await expect(page.locator('text=Welcome back')).toBeVisible();
});

// Multiple browsers
test.describe('Cross-browser tests', () => {
  test('works in all browsers', async ({ page, browserName }) => {
    console.log(`Testing in ${browserName}`);
    await page.goto('/');
    await expect(page.locator('h1')).toBeVisible();
  });
});

// Mobile testing
test('mobile menu works', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 });
  await page.goto('/');
  
  await page.click('.mobile-menu-button');
  await expect(page.locator('.mobile-menu')).toBeVisible();
});

// API testing
test('API returns correct data', async ({ request }) => {
  const response = await request.get('/api/users/1');
  expect(response.ok()).toBeTruthy();
  
  const data = await response.json();
  expect(data).toHaveProperty('id', 1);
  expect(data).toHaveProperty('name');
});

// Screenshots and videos
test('capture screenshot', async ({ page }) => {
  await page.goto('/');
  await page.screenshot({ path: 'homepage.png' });
});

7. Test Coverage & CI/CD

// Jest coverage
// package.json
{
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx}",
      "!src/index.js",
      "!src/**/*.test.{js,jsx}"
    ],
    "coverageThresholds": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

// Run coverage
npm run test:coverage

// GitHub Actions CI
// .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    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 unit tests
        run: npm test
      
      - name: Run integration tests
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
        run: npm run test:integration
      
      - name: Run E2E tests
        run: |
          npm run build
          npm run start &
          npx wait-on http://localhost:3000
          npm run cypress:run
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json

// Test parallelization
// package.json
{
  "scripts": {
    "test": "jest --maxWorkers=4",
    "test:ci": "jest --ci --maxWorkers=2"
  }
}

8. Best Practices

βœ“ Testing Best Practices:

Conclusion

Automated testing is essential for maintainable software. Implement unit tests for individual functions, integration tests for API endpoints, and E2E tests for critical user flows. Use mocking to isolate dependencies, maintain high code coverage, and run tests in CI/CD pipelines. Well-tested code gives confidence for refactoring and prevents regressions.

πŸ’‘ Pro Tip: Use Test-Driven Development (TDD) for critical features: write failing tests first, then implement code to make them pass. This ensures testable code design and prevents untested paths. For complex business logic, consider property-based testing with libraries like fast-check to automatically generate test cases. Set up mutation testing with Stryker to verify your tests actually catch bugsβ€”it modifies your code and checks if tests fail appropriately.