Complete Testing Strategies: From Unit Tests to E2E

Testing isn’t just about catching bugs—it’s about building confidence in your code. Let’s explore comprehensive testing strategies that will make your applications more reliable and maintainable.

Testing Pyramid and Strategy

Understanding the Testing Pyramid

// Unit Tests (70%) - Fast, isolated, numerous
// Integration Tests (20%) - Medium speed, test component interactions  
// E2E Tests (10%) - Slow, expensive, test complete user flows

// Example test distribution for a typical application:
// - 100+ unit tests
// - 20-30 integration tests  
// - 5-10 E2E tests

Unit Testing Best Practices

Jest Testing Patterns

// userService.js
class UserService {
  constructor(userRepository, emailService) {
    this.userRepository = userRepository;
    this.emailService = emailService;
  }
  
  async createUser(userData) {
    // Validate input
    if (!userData.email || !userData.name) {
      throw new Error('Email and name are required');
    }
    
    // Check if user exists
    const existingUser = await this.userRepository.findByEmail(userData.email);
    if (existingUser) {
      throw new Error('User already exists');
    }
    
    // Create user
    const user = await this.userRepository.create({
      ...userData,
      id: this.generateId(),
      createdAt: new Date()
    });
    
    // Send welcome email
    await this.emailService.sendWelcomeEmail(user.email, user.name);
    
    return user;
  }
  
  generateId() {
    return Math.random().toString(36).substr(2, 9);
  }
}

// userService.test.js
describe('UserService', () => {
  let userService;
  let mockUserRepository;
  let mockEmailService;
  
  beforeEach(() => {
    // Create mocks
    mockUserRepository = {
      findByEmail: jest.fn(),
      create: jest.fn()
    };
    
    mockEmailService = {
      sendWelcomeEmail: jest.fn()
    };
    
    userService = new UserService(mockUserRepository, mockEmailService);
  });
  
  describe('createUser', () => {
    const validUserData = {
      email: 'test@example.com',
      name: 'Test User'
    };
    
    it('should create a user successfully', async () => {
      // Arrange
      mockUserRepository.findByEmail.mockResolvedValue(null);
      mockUserRepository.create.mockResolvedValue({
        ...validUserData,
        id: 'user123',
        createdAt: expect.any(Date)
      });
      
      // Act
      const result = await userService.createUser(validUserData);
      
      // Assert
      expect(mockUserRepository.findByEmail).toHaveBeenCalledWith(validUserData.email);
      expect(mockUserRepository.create).toHaveBeenCalledWith({
        ...validUserData,
        id: expect.any(String),
        createdAt: expect.any(Date)
      });
      expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
        validUserData.email,
        validUserData.name
      );
      expect(result).toEqual(expect.objectContaining(validUserData));
    });
    
    it('should throw error when email is missing', async () => {
      // Arrange
      const invalidUserData = { name: 'Test User' };
      
      // Act & Assert
      await expect(userService.createUser(invalidUserData))
        .rejects.toThrow('Email and name are required');
      
      expect(mockUserRepository.findByEmail).not.toHaveBeenCalled();
      expect(mockUserRepository.create).not.toHaveBeenCalled();
    });
    
    it('should throw error when user already exists', async () => {
      // Arrange
      mockUserRepository.findByEmail.mockResolvedValue({ id: 'existing' });
      
      // Act & Assert
      await expect(userService.createUser(validUserData))
        .rejects.toThrow('User already exists');
      
      expect(mockUserRepository.create).not.toHaveBeenCalled();
      expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled();
    });
    
    it('should handle email service failure gracefully', async () => {
      // Arrange
      mockUserRepository.findByEmail.mockResolvedValue(null);
      mockUserRepository.create.mockResolvedValue(validUserData);
      mockEmailService.sendWelcomeEmail.mockRejectedValue(new Error('Email failed'));
      
      // Act & Assert
      await expect(userService.createUser(validUserData))
        .rejects.toThrow('Email failed');
    });
  });
});

Testing Async Code

// Testing promises
describe('Async operations', () => {
  it('should handle async/await', async () => {
    const result = await fetchUserData('123');
    expect(result).toEqual(expect.objectContaining({
      id: '123',
      name: expect.any(String)
    }));
  });
  
  it('should handle promise rejection', async () => {
    await expect(fetchUserData('invalid'))
      .rejects.toThrow('User not found');
  });
  
  it('should timeout long operations', async () => {
    jest.setTimeout(10000); // 10 seconds
    
    const result = await longRunningOperation();
    expect(result).toBeDefined();
  }, 10000);
});

// Testing callbacks
describe('Callback functions', () => {
  it('should handle callback success', (done) => {
    fetchUserWithCallback('123', (error, user) => {
      expect(error).toBeNull();
      expect(user).toEqual(expect.objectContaining({
        id: '123'
      }));
      done();
    });
  });
  
  it('should handle callback error', (done) => {
    fetchUserWithCallback('invalid', (error, user) => {
      expect(error).toBeInstanceOf(Error);
      expect(user).toBeUndefined();
      done();
    });
  });
});

Integration Testing

API Integration Tests

// api.test.js
const request = require('supertest');
const app = require('../app');
const { setupTestDB, cleanupTestDB } = require('./helpers/database');

describe('User API Integration Tests', () => {
  beforeAll(async () => {
    await setupTestDB();
  });
  
  afterAll(async () => {
    await cleanupTestDB();
  });
  
  beforeEach(async () => {
    // Clean up data between tests
    await User.deleteMany({});
  });
  
  describe('POST /api/users', () => {
    it('should create a new user', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'SecurePass123!'
      };
      
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);
      
      expect(response.body).toEqual({
        success: true,
        data: expect.objectContaining({
          id: expect.any(String),
          name: userData.name,
          email: userData.email
        })
      });
      
      // Verify user was created in database
      const user = await User.findOne({ email: userData.email });
      expect(user).toBeTruthy();
      expect(user.name).toBe(userData.name);
    });
    
    it('should return validation error for invalid data', async () => {
      const invalidData = {
        name: 'J', // Too short
        email: 'invalid-email',
        password: '123' // Too weak
      };
      
      const response = await request(app)
        .post('/api/users')
        .send(invalidData)
        .expect(400);
      
      expect(response.body).toEqual({
        success: false,
        error: 'Validation failed',
        details: expect.arrayContaining([
          expect.objectContaining({
            field: 'name',
            message: expect.stringContaining('at least 2 characters')
          }),
          expect.objectContaining({
            field: 'email',
            message: expect.stringContaining('valid email')
          })
        ])
      });
    });
  });
  
  describe('GET /api/users/:id', () => {
    let createdUser;
    
    beforeEach(async () => {
      createdUser = await User.create({
        name: 'Test User',
        email: 'test@example.com',
        password: 'hashedpassword'
      });
    });
    
    it('should return user by id', async () => {
      const response = await request(app)
        .get(`/api/users/${createdUser.id}`)
        .expect(200);
      
      expect(response.body).toEqual({
        success: true,
        data: expect.objectContaining({
          id: createdUser.id,
          name: createdUser.name,
          email: createdUser.email
        })
      });
    });
    
    it('should return 404 for non-existent user', async () => {
      const response = await request(app)
        .get('/api/users/nonexistent')
        .expect(404);
      
      expect(response.body).toEqual({
        success: false,
        error: 'User not found'
      });
    });
  });
});

Database Integration Tests

// database.test.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const User = require('../models/User');

describe('User Model Integration', () => {
  let mongoServer;
  
  beforeAll(async () => {
    mongoServer = await MongoMemoryServer.create();
    const mongoUri = mongoServer.getUri();
    await mongoose.connect(mongoUri);
  });
  
  afterAll(async () => {
    await mongoose.disconnect();
    await mongoServer.stop();
  });
  
  beforeEach(async () => {
    await User.deleteMany({});
  });
  
  describe('User creation', () => {
    it('should create user with valid data', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'hashedpassword'
      };
      
      const user = new User(userData);
      const savedUser = await user.save();
      
      expect(savedUser._id).toBeDefined();
      expect(savedUser.name).toBe(userData.name);
      expect(savedUser.email).toBe(userData.email);
      expect(savedUser.createdAt).toBeInstanceOf(Date);
    });
    
    it('should enforce unique email constraint', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'hashedpassword'
      };
      
      await User.create(userData);
      
      // Try to create another user with same email
      await expect(User.create(userData))
        .rejects.toThrow(/duplicate key error/);
    });
    
    it('should validate required fields', async () => {
      const invalidUser = new User({
        name: 'John Doe'
        // Missing email and password
      });
      
      await expect(invalidUser.save())
        .rejects.toThrow(/validation failed/);
    });
  });
  
  describe('User queries', () => {
    beforeEach(async () => {
      await User.create([
        { name: 'John Doe', email: 'john@example.com', password: 'hash1' },
        { name: 'Jane Smith', email: 'jane@example.com', password: 'hash2' },
        { name: 'Bob Johnson', email: 'bob@example.com', password: 'hash3' }
      ]);
    });
    
    it('should find user by email', async () => {
      const user = await User.findOne({ email: 'john@example.com' });
      
      expect(user).toBeTruthy();
      expect(user.name).toBe('John Doe');
    });
    
    it('should find users with pagination', async () => {
      const users = await User.find()
        .limit(2)
        .skip(1)
        .sort({ name: 1 });
      
      expect(users).toHaveLength(2);
      expect(users[0].name).toBe('Jane Smith');
      expect(users[1].name).toBe('John Doe');
    });
  });
});

End-to-End Testing

Cypress E2E Tests

// cypress/integration/user-registration.spec.js
describe('User Registration Flow', () => {
  beforeEach(() => {
    // Reset database state
    cy.task('db:seed');
    cy.visit('/register');
  });
  
  it('should register a new user successfully', () => {
    // Fill out registration form
    cy.get('[data-testid="name-input"]').type('John Doe');
    cy.get('[data-testid="email-input"]').type('john@example.com');
    cy.get('[data-testid="password-input"]').type('SecurePass123!');
    cy.get('[data-testid="confirm-password-input"]').type('SecurePass123!');
    
    // Submit form
    cy.get('[data-testid="register-button"]').click();
    
    // Verify success message
    cy.get('[data-testid="success-message"]')
      .should('be.visible')
      .and('contain', 'Registration successful');
    
    // Verify redirect to login page
    cy.url().should('include', '/login');
    
    // Verify user can login
    cy.get('[data-testid="email-input"]').type('john@example.com');
    cy.get('[data-testid="password-input"]').type('SecurePass123!');
    cy.get('[data-testid="login-button"]').click();
    
    // Verify successful login
    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="user-name"]').should('contain', 'John Doe');
  });
  
  it('should show validation errors for invalid input', () => {
    // Try to submit empty form
    cy.get('[data-testid="register-button"]').click();
    
    // Check validation errors
    cy.get('[data-testid="name-error"]')
      .should('be.visible')
      .and('contain', 'Name is required');
    
    cy.get('[data-testid="email-error"]')
      .should('be.visible')
      .and('contain', 'Email is required');
    
    // Fill invalid email
    cy.get('[data-testid="email-input"]').type('invalid-email');
    cy.get('[data-testid="register-button"]').click();
    
    cy.get('[data-testid="email-error"]')
      .should('contain', 'Please enter a valid email');
  });
  
  it('should handle server errors gracefully', () => {
    // Mock server error
    cy.intercept('POST', '/api/users', {
      statusCode: 500,
      body: { error: 'Internal server error' }
    });
    
    // Fill form and submit
    cy.get('[data-testid="name-input"]').type('John Doe');
    cy.get('[data-testid="email-input"]').type('john@example.com');
    cy.get('[data-testid="password-input"]').type('SecurePass123!');
    cy.get('[data-testid="confirm-password-input"]').type('SecurePass123!');
    cy.get('[data-testid="register-button"]').click();
    
    // Verify error message
    cy.get('[data-testid="error-message"]')
      .should('be.visible')
      .and('contain', 'Something went wrong');
  });
});

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

Cypress.Commands.add('createUser', (userData) => {
  cy.request('POST', '/api/users', userData);
});

Playwright E2E Tests

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

test.describe('User Management Flow', () => {
  test.beforeEach(async ({ page }) => {
    // Setup test data
    await page.request.post('/api/test/reset-db');
    await page.goto('/');
  });
  
  test('complete user journey', async ({ page }) => {
    // Register new user
    await page.click('text=Sign Up');
    await page.fill('[data-testid="name"]', 'Test User');
    await page.fill('[data-testid="email"]', 'test@example.com');
    await page.fill('[data-testid="password"]', 'SecurePass123!');
    await page.click('[data-testid="register"]');
    
    // Verify registration success
    await expect(page.locator('[data-testid="success-message"]'))
      .toContainText('Registration successful');
    
    // Login
    await page.fill('[data-testid="email"]', 'test@example.com');
    await page.fill('[data-testid="password"]', 'SecurePass123!');
    await page.click('[data-testid="login"]');
    
    // Verify dashboard access
    await expect(page).toHaveURL(/.*dashboard/);
    await expect(page.locator('[data-testid="welcome-message"]'))
      .toContainText('Welcome, Test User');
    
    // Update profile
    await page.click('[data-testid="profile-link"]');
    await page.fill('[data-testid="bio"]', 'This is my bio');
    await page.click('[data-testid="save-profile"]');
    
    // Verify profile update
    await expect(page.locator('[data-testid="success-toast"]'))
      .toContainText('Profile updated successfully');
    
    // Logout
    await page.click('[data-testid="logout"]');
    await expect(page).toHaveURL('/');
  });
  
  test('should handle network failures', async ({ page }) => {
    // Simulate network failure
    await page.route('/api/users', route => {
      route.abort('failed');
    });
    
    await page.click('text=Sign Up');
    await page.fill('[data-testid="name"]', 'Test User');
    await page.fill('[data-testid="email"]', 'test@example.com');
    await page.fill('[data-testid="password"]', 'SecurePass123!');
    await page.click('[data-testid="register"]');
    
    // Verify error handling
    await expect(page.locator('[data-testid="error-message"]'))
      .toContainText('Network error occurred');
  });
});

Test Utilities and Helpers

Test Data Factories

// testUtils/factories.js
const faker = require('faker');

class UserFactory {
  static create(overrides = {}) {
    return {
      id: faker.datatype.uuid(),
      name: faker.name.findName(),
      email: faker.internet.email(),
      password: 'hashedpassword',
      createdAt: faker.date.past(),
      ...overrides
    };
  }
  
  static createMany(count, overrides = {}) {
    return Array.from({ length: count }, () => this.create(overrides));
  }
}

class PostFactory {
  static create(overrides = {}) {
    return {
      id: faker.datatype.uuid(),
      title: faker.lorem.sentence(),
      content: faker.lorem.paragraphs(3),
      authorId: faker.datatype.uuid(),
      published: faker.datatype.boolean(),
      createdAt: faker.date.past(),
      ...overrides
    };
  }
}

// Usage in tests
describe('User tests', () => {
  it('should handle multiple users', () => {
    const users = UserFactory.createMany(5);
    const adminUser = UserFactory.create({ role: 'admin' });
    
    // Test logic here
  });
});

Custom Test Matchers

// testUtils/matchers.js
expect.extend({
  toBeValidEmail(received) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = emailRegex.test(received);
    
    if (pass) {
      return {
        message: () => `expected ${received} not to be a valid email`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to be a valid email`,
        pass: false,
      };
    }
  },
  
  toHaveValidationError(received, field) {
    const hasError = received.errors && 
                    received.errors.some(error => error.field === field);
    
    if (hasError) {
      return {
        message: () => `expected response not to have validation error for ${field}`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected response to have validation error for ${field}`,
        pass: false,
      };
    }
  }
});

// Usage
test('email validation', () => {
  expect('test@example.com').toBeValidEmail();
  expect('invalid-email').not.toBeValidEmail();
});

test('API validation', async () => {
  const response = await request(app)
    .post('/api/users')
    .send({ name: 'Test' }); // Missing email
  
  expect(response.body).toHaveValidationError('email');
});

Testing is an investment in code quality and team confidence. Start with unit tests for your core business logic, add integration tests for critical paths, and use E2E tests to verify complete user workflows. Remember: good tests are fast, reliable, and maintainable.