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.