End‑to‑end (E2E) testing simulates real user interactions with your application.
This tutorial covers E2E testing with Cypress and Playwright, the Test‑Driven
Development (TDD) workflow, and test coverage metrics. You'll learn to write
comprehensive tests that validate your entire application from the user's
perspective.
Why this matters:
E2E tests catch integration issues that unit tests miss. TDD helps you design
better code and reduce bugs. Test coverage shows you which parts of your
codebase need more tests.
1. End‑to‑End Testing
E2E tests run the entire application (frontend + backend + database) and
simulate user interactions.
Benefits: Catches integration bugs, validates user flows, builds user
confidence.
Challenges: Slower to run, more complex setup, flaky tests.
Tools: Cypress, Playwright, Selenium, Puppeteer.
// E2E Test Example (user flow)
// 1. Visit the login page
// 2. Enter username and password
// 3. Click login button
// 4. Verify user is redirected to dashboard
// 5. Verify dashboard shows user's name
E2E best practices:
Test critical user journeys (happy paths)
Use test data that is isolated from production
Run E2E tests in CI/CD pipelines
Avoid testing implementation details
2. Cypress
Cypress is a modern E2E testing framework with a developer‑friendly API.
# Installation
npm install --save-dev cypress
npx cypress open
Playwright: Cross‑browser support (Chrome, Firefox, Safari), better for
CI/CD, supports mobile emulation.
4. Test‑Driven Development (TDD)
TDD is a development practice where you write tests before writing code.
The Red‑Green‑Refactor cycle
Red: Write a failing test for the feature.
Green: Write the minimum code to make the test pass.
Refactor: Improve the code while keeping tests green.
// Step 1: RED – Write a failing test (calculator.test.ts)
import { calculate } from './calculator';
describe('Calculator', () => {
it('adds two numbers correctly', () => {
expect(calculate(2, 3, 'add')).toBe(5);
});
});
// Step 2: GREEN – Write code to pass the test
export function calculate(a: number, b: number, operation: string): number {
if (operation === 'add') return a + b;
return 0;
}
// Step 3: REFACTOR – Improve code
export function calculate(a: number, b: number, operation: string): number {
const operations: Record number> = {
'add': (a, b) => a + b,
'subtract': (a, b) => a - b,
'multiply': (a, b) => a * b,
'divide': (a, b) => a / b,
};
return operations[operation]?.(a, b) ?? 0;
}
TDD Best Practices
Write small, focused tests: Test one thing at a time.
Run tests frequently: Every few minutes.
Don't skip the refactor step: It's where code quality improves.
Tests drive design: TDD often leads to better, more modular code.
5. Test Coverage & Metrics
Test coverage measures how much of your code is exercised by tests.
Coverage metrics
Line coverage: Percentage of lines executed.
Branch coverage: Percentage of branches (if/else) executed.
Function coverage: Percentage of functions called.
Statement coverage: Percentage of statements executed.
Remember: High coverage doesn't guarantee good tests. A test
can have 100% coverage and still miss important bugs. Focus on what
you're testing, not just coverage numbers.
Quiz
Question 1
What is the first step in the TDD Red‑Green‑Refactor cycle?
Refactor the code
Write a failing test
Write code to pass the test
Deploy to production
Show answer
B. Write a failing test (Red).
Question 2
Which E2E testing framework offers the best cross‑browser support?
Cypress
Playwright
Selenium
Puppeteer
Show answer
B. Playwright (supports Chromium, Firefox, and WebKit).
Question 3
What does branch coverage measure?
Percentage of lines executed
Percentage of branches (if/else) executed
Percentage of functions called
Percentage of files tested
Show answer
B. Percentage of branches (if/else) executed.
Exercises
Exercise 1
Write a Cypress test that visits the home page, clicks a "Contact" link,
fills out a contact form, and verifies the success message.
Sample answer
describe('Contact Form', () => {
it('submits the contact form successfully', () => {
cy.visit('/');
cy.contains('Contact').click();
cy.url().should('include', '/contact');
cy.get('[data-testid="name-input"]').type('John Doe');
cy.get('[data-testid="email-input"]').type('john@example.com');
cy.get('[data-testid="message-input"]').type('Hello, this is a test message.');
cy.get('[data-testid="submit-button"]').click();
cy.contains('Thank you for your message!').should('be.visible');
});
});
Exercise 2
Apply TDD to write a isValidEmail function. Start with a
failing test, then implement the function, then refactor.
Sample answer
// Step 1: RED - Write failing test
import { isValidEmail } from './validation';
describe('isValidEmail', () => {
it('returns true for valid emails', () => {
expect(isValidEmail('test@example.com')).toBe(true);
expect(isValidEmail('user.name@domain.co')).toBe(true);
});
it('returns false for invalid emails', () => {
expect(isValidEmail('invalid')).toBe(false);
expect(isValidEmail('test@')).toBe(false);
expect(isValidEmail('@example.com')).toBe(false);
});
});
// Step 2: GREEN - Minimum implementation
export function isValidEmail(email: string): boolean {
return email.includes('@') && email.includes('.');
}
// Step 3: REFACTOR - Better validation
export function isValidEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
Homework
Homework 1
Write E2E tests for the critical user flows of your capstone project using
Cypress or Playwright. Include at least 3 user flows (login, create item, view dashboard).
Sample outline
Flow 1: User registration and login
Flow 2: Create a new item (post, product, task)
Flow 3: View and interact with the dashboard
Setup: Test data seeding, clean‑up after tests
Mini‑Project
TDD Practice: String Calculator
Build a String Calculator using TDD:
add("") → 0
add("1") → 1
add("1,2") → 3
add("1,2,3") → 6
add("1\n2,3") → 6 (support newlines)
add("//;\n1;2") → 3 (support custom delimiters)
Negative numbers throw an exception
Write one test at a time, then implement. Refactor between steps.
Sample solution outline
Step 1: Test empty string → 0. Implement.
Step 2: Test single number → returns number.
Step 3: Test two numbers → sum.
Step 4: Test multiple numbers → sum.
Step 5: Test newline support.
Step 6: Test custom delimiter support.
Step 7: Test negative numbers → throw error.
This exercise demonstrates TDD's incremental development approach.
Tutorial Summary
You learned end‑to‑end testing with Cypress and Playwright, the Test‑Driven
Development workflow (Red‑Green‑Refactor), and test coverage metrics.
These advanced testing techniques are essential for building high‑quality,
reliable applications that users can trust.
Key takeaway: Testing is a mindset, not a task. Whether
you're writing unit tests, integration tests, or E2E tests, the goal is
the same: to build software with confidence and reduce the cost of bugs.