Unit 6.4 · Tutorial 2

E2E Testing & Test‑Driven Development

Chapter 20 · Advanced Topics
~3 hours Advanced Cypress · Playwright · TDD · Coverage

Overview

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
// cypress/e2e/login.spec.js describe('Login Flow', () => { beforeEach(() => { cy.visit('/login'); }); it('logs in successfully with valid credentials', () => { cy.get('[data-testid="email-input"]').type('user@example.com'); cy.get('[data-testid="password-input"]').type('password123'); cy.get('[data-testid="login-button"]').click(); cy.url().should('include', '/dashboard'); cy.contains('Welcome, User!').should('be.visible'); }); it('shows an error with invalid credentials', () => { cy.get('[data-testid="email-input"]').type('wrong@example.com'); cy.get('[data-testid="password-input"]').type('wrong'); cy.get('[data-testid="login-button"]').click(); cy.contains('Invalid credentials').should('be.visible'); }); });
// cypress/support/commands.js // Custom commands 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(); }); // Using the custom command it('logs in and creates a post', () => { cy.login('user@example.com', 'password123'); cy.get('[data-testid="new-post-button"]').click(); // ... continue the flow });

Stubbing and Fixtures

// cypress/fixtures/users.json { "id": 1, "name": "Alice", "email": "alice@ex.com" } // In test cy.intercept('GET', '/api/users', { fixture: 'users' });

3. Playwright

Playwright is a cross‑browser E2E testing framework with excellent multi‑browser support.

# Installation npm init playwright@latest npx playwright test
// tests/login.spec.ts import { test, expect } from '@playwright/test'; test.describe('Login Flow', () => { test('logs in with valid credentials', async ({ page }) => { await page.goto('/login'); await page.fill('[data-testid="email-input"]', 'user@example.com'); await page.fill('[data-testid="password-input"]', 'password123'); await page.click('[data-testid="login-button"]'); await expect(page).toHaveURL(/.*dashboard/); await expect(page.locator('text=Welcome, User!')).toBeVisible(); }); test('shows error with invalid credentials', async ({ page }) => { await page.goto('/login'); await page.fill('[data-testid="email-input"]', 'wrong@example.com'); await page.fill('[data-testid="password-input"]', 'wrong'); await page.click('[data-testid="login-button"]'); await expect(page.locator('text=Invalid credentials')).toBeVisible(); }); // Multiple browsers test('works in all browsers', async ({ browserName }) => { console.log(`Running in: ${browserName}`); // Test runs in Chromium, Firefox, WebKit }); });
// playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', }, projects: [ { name: 'chromium', use: { browserName: 'chromium' } }, { name: 'firefox', use: { browserName: 'firefox' } }, { name: 'webkit', use: { browserName: 'webkit' } }, ], });
Cypress vs Playwright:
  • Cypress: Developer‑friendly, excellent debugging, real‑time reload.
  • 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.
# Generate coverage with Jest jest --coverage # Generate coverage with Vitest vitest --coverage # Output example -----------------|---------|----------|---------|---------| File | % Stmts | % Branch | % Funcs | % Lines | -----------------|---------|----------|---------|---------| All files | 85.71 | 60 | 80 | 85.71 | src | 85.71 | 60 | 80 | 85.71 | calculator.ts | 100 | 100 | 100 | 100 | utils.ts | 71.43 | 50 | 60 | 71.43 | -----------------|---------|----------|---------|---------|

Coverage thresholds

// jest.config.js module.exports = { collectCoverage: true, collectCoverageFrom: [ 'src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/index.ts', ], coverageThreshold: { global: { statements: 80, branches: 70, functions: 80, lines: 80, }, }, };
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.