Post

End-to-End (E2E) Testing in Depth - Part 4

If unit tests check individual functions, and integration tests check how those functions work together, end-to-end (E2E) tests take it all the way: They simulate real user workflows from start to finish, ensuring the entire application stack works as expected.

E2E testing is about validating the system as a whole, covering everything from frontend to backend, database, APIs, and sometimes even external services.

What Are E2E Tests?

E2E tests replicate user behavior and verify that the system responds correctly. Think of them as automated users interacting with your app.

Example workflows tested in E2E:

  • User logs in → Dashboard loads correctly.
  • User creates a record → It’s stored in DB → Appears in the UI.
  • Checkout flow in an e-commerce app → Product added → Payment processed → Order confirmed.

Why E2E Tests Matter

  • Validate Real Workflows → Ensure the system behaves like users expect.
  • Catch System-Wide Failures → Config issues, routing errors, DB schema mismatches.
  • Test Critical Paths → Login, payments, onboarding, search, etc.
  • Provide Business Confidence → Stakeholders see features working as intended.

Characteristics of E2E Tests

  • High Coverage – Test across the full stack.
  • Slowest of All Tests – Often run in CI/CD pipelines, not during active coding.
  • Brittle – Small UI changes can break tests, so balance is key.
  • Mimic Real User Behavior – Often browser-driven or API-driven.

Side-by-Side Examples

Scenario:

A user visits a web app, logs in, and sees their profile page with their name.

Python (Selenium + pytest)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# test_e2e_login.py
from selenium import webdriver
from selenium.webdriver.common.by import By
import pytest

@pytest.fixture
def driver():
    driver = webdriver.Chrome()
    yield driver
    driver.quit()

def test_login_flow(driver):
    driver.get("http://localhost:5000/login")

    driver.find_element(By.NAME, "username").send_keys("alice")
    driver.find_element(By.NAME, "password").send_keys("password123")
    driver.find_element(By.ID, "login-button").click()

    profile_name = driver.find_element(By.ID, "profile-name").text
    assert profile_name == "Alice"

C# (Selenium + xUnit)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using Xunit;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

public class LoginE2ETests
{
    [Fact]
    public void Login_ShowsProfilePage()
    {
        using var driver = new ChromeDriver();
        driver.Navigate().GoToUrl("http://localhost:5000/login");

        driver.FindElement(By.Name("username")).SendKeys("alice");
        driver.FindElement(By.Name("password")).SendKeys("password123");
        driver.FindElement(By.Id("login-button")).Click();

        var profileName = driver.FindElement(By.Id("profile-name")).Text;
        Assert.Equal("Alice", profileName);
    }
}

TypeScript (Playwright / Cypress)

(Playwright example below — Cypress would be very similar)

1
2
3
4
5
6
7
8
9
10
11
12
13
// login.e2e.test.ts
import { test, expect } from "@playwright/test";

test("user can login and see profile page", async ({ page }) => {
  await page.goto("http://localhost:3000/login");

  await page.fill("input[name=username]", "alice");
  await page.fill("input[name=password]", "password123");
  await page.click("#login-button");

  const profileName = await page.textContent("#profile-name");
  expect(profileName).toBe("Alice");
});

PHP (Laravel Dusk for browser automation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// tests/Browser/LoginTest.php
<?php

namespace Tests\Browser;

use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class LoginTest extends DuskTestCase
{
    public function testUserCanLoginAndSeeProfile()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/login')
                    ->type('username', 'alice')
                    ->type('password', 'password123')
                    ->press('Login')
                    ->assertSee('Alice');
        });
    }
}

Comparison Table

AspectE2E Testing GoalPython (Selenium)C# (Selenium + xUnit)TypeScript (Playwright/Cypress)PHP (Laravel Dusk)
ScopeFull user workflow (UI → backend → DB)
ToolsSeleniumSeleniumPlaywright/CypressDusk (browser automation) 
SpeedSlow (browser interaction)SlowMediumMedium/Fast 
ConfidenceHighest — validates the system end-to-end⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Best ForLogin flows, payments, user journeysUI + API flowsUI + API flowsFullstack web appsLaravel apps

Best Practices for Writing Effective E2E Tests

  1. Test Critical User Journeys Only
    • Focus on login, checkout, payments, onboarding, etc.
    • Don’t waste time E2E testing every small feature.
  2. Keep Tests Deterministic
    • Avoid flakiness by controlling test data.
    • Seed databases with known test records.
  3. Use Test-Specific Environments
    • Run E2E tests in staging or CI environments.
    • Isolate from production data and services.
  4. Clean Up After Tests
    • Ensure created records (users, orders) are rolled back or deleted.
  5. Use IDs and Stable Selectors
    • Avoid brittle selectors like div:nth-child(3).
    • Use unique IDs or data-test attributes.
  6. Parallelize and Optimize
    • Run tests in parallel (e.g., Playwright, Cypress).
    • Keep them minimal to avoid bloated CI runs.
  7. Combine with Unit & Integration Tests
    • Don’t rely solely on E2E.
    • Use a test pyramid strategy:

      • 70% Unit Tests
      • 20% Integration Tests
      • 10% E2E Tests
  8. Monitor and Review Regularly
    • E2E tests can get brittle.
    • Review and refactor when the UI or workflows change.

Key Takeaways

  • E2E tests replicate the user’s journey and validate the full system.
  • They are slower and more brittle, but they give the highest level of confidence.
  • Use them sparingly for critical paths (login, payments, core workflows).
  • Balance your test pyramid:

    • Many unit tests
    • Fewer integration tests
    • Very few but powerful E2E tests
  • Choose the right tools for your stack (Selenium, Playwright, Cypress, Dusk).
  • Follow best practices to keep tests reliable and maintainable.
This post is licensed under CC BY 4.0 by the author.