Critical user journeys, not test-pyramid theater
E2E tests with Cypress or Playwright secure a shop's most critical purchase flows, but used the wrong way they cause more maintenance work than value. This article shows which user journeys, like checkout, login, and search, deserve real end-to-end coverage, which cases are better checked at the unit or integration level, and how to justify the investment to stakeholders with clear numbers.
Table of Contents
- 1. The core question: when is an E2E test really worth it
- 2. Identifying critical user journeys
- 3. Checkout, login, and search as prime examples
- 4. Edge cases: why they rarely belong in the E2E suite
- 5. The underestimated maintenance cost of E2E tests
- 6. Decision criteria: what should NOT be E2E-tested
- 7. Making the ROI case to stakeholders
- 8. Embedding E2E tests in the development process
- 9. Journey prioritization, side by side
- 10. Summary
- 11. FAQ
1. The core question: when is an E2E test really worth it
An end-to-end test simulates a real user in the browser: clicks, form entries, network requests, and the full interaction between frontend, backend, database, and third-party services like payment providers. That makes E2E tests the only test type that truly proves a feature works from the user's point of view. But that exact strength is also its biggest weakness: a Cypress or Playwright test that runs through a complete checkout needs a running application, a test database, often external test environments for payment and shipping services, and several seconds of runtime per run. A comparable unit test for the same price calculation runs in milliseconds with no infrastructure at all. So the core question isn't "how do we test this most thoroughly", it's "what risk justifies the cost of an end-to-end test".
The test pyramid remains the right heuristic here, even though many teams flip it upside down in practice. The base is made up of many fast unit tests that check individual functions and calculations in isolation. Above that sit integration tests, which secure the interplay of a handful of components, such as a service and its database layer. At the top sit a small number of E2E tests, covering exclusively the journeys whose failure would immediately hit revenue or users' core trust. Flip that order, make E2E tests the main safety net, and you end up with a suite that breaks on every small UI change, takes hours to run in full, and tempts developers to ignore red tests instead of fixing them.
In practice, an E2E test is worth it when three conditions hold at the same time: the failure case affects a business-critical journey, the bug is only detectable through the real interplay of multiple systems, and the journey changes rarely enough to justify the maintenance cost. If any one of these conditions is missing, a unit or integration test is almost always the better choice. These three criteria run through the rest of this article and form the basis for the decision matrix in the section on journey prioritization.
2. Identifying critical user journeys
A critical user journey isn't a gut call, it can be derived from three data sources: funnel analytics, support tickets, and revenue attribution. Google Analytics or a comparable tool shows which steps in the purchase process have the highest drop-off rate and how many users actually go through that path at all. Support tickets reveal which failures have actually affected customers in the past, not just failures that were theoretically possible. Revenue attribution shows what share of total revenue is generated through which path. A journey that two percent of users go through but that accounts for five percent of revenue deserves different priority than a journey with similar traffic but no direct revenue link, such as a wishlist feature.
A second criterion is irreversibility: journeys where a failure can't be undone deserve disproportionate attention. A duplicate payment at checkout, a lost cart after a session timeout, or a wrong address sent to the shipping provider all cause direct damage and support overhead that goes far beyond the original development time. Journeys with retryability, such as a failed product filter that a user simply tries again, less often justify the cost of full E2E coverage. A useful thought experiment: if this step failed overnight, how many support tickets, how much lost revenue, and how much reputational damage would accumulate before anyone even noticed the problem?
In practice, most e-commerce applications converge on the same four to six journeys as critical: product search with filtering, cart and checkout, login and registration, and payment-related steps like coupon redemption and shipping cost calculation. Everything outside that core set, from review forms to social sharing buttons, generally doesn't belong in the E2E suite, and is instead covered by targeted unit and component tests plus manual exploratory testing.
3. Checkout, login, and search as prime examples
The checkout process is the textbook example of a worthwhile E2E test, because it involves several systems at once: the cart service, price calculation with taxes and discounts, payment provider integration, stock checks, and order confirmation emails. A unit test can check each of these components in isolation, but only an E2E test proves that they actually work together, when a real browser fills out the forms and the application reacts to real, if simulated, responses from the payment provider. Exactly these kinds of integration bugs, a session token incorrectly passed between the cart and the payment redirect, for instance, simply never show up in unit tests, because there each component works in isolation with mocks.
// cypress/e2e/checkout.cy.js
// Critical journey: guest checkout with a valid credit card
describe('Checkout - guest purchase', () => {
beforeEach(() => {
// Seed cart via API instead of clicking through the catalog
cy.request('POST', '/rest/V1/guest-carts', {}).then((cartRes) => {
const cartId = cartRes.body;
cy.request('POST', `/rest/V1/guest-carts/${cartId}/items`, {
cartItem: { sku: 'MS-1234', qty: 1, quote_id: cartId },
});
cy.wrap(cartId).as('cartId');
});
});
it('completes checkout end to end and shows the order confirmation', () => {
cy.visit('/checkout');
// Shipping address
cy.get('[data-testid="email-input"]').type('guest@example.com');
cy.get('[data-testid="firstname-input"]').type('Max');
cy.get('[data-testid="lastname-input"]').type('Mustermann');
cy.get('[data-testid="street-input"]').type('Teststrasse 1');
cy.get('[data-testid="postcode-input"]').type('10115');
cy.get('[data-testid="city-input"]').type('Berlin');
cy.get('[data-testid="continue-to-shipping"]').click();
// Shipping method
cy.get('[data-testid="shipping-method-standard"]').click();
cy.get('[data-testid="continue-to-payment"]').click();
// Payment: use the sandbox test card
cy.get('[data-testid="card-number-input"]').type('4242424242424242');
cy.get('[data-testid="card-expiry-input"]').type('12/30');
cy.get('[data-testid="card-cvc-input"]').type('123');
cy.get('[data-testid="place-order-button"]').click();
// Assert the full journey succeeded
cy.url({ timeout: 10000 }).should('include', '/checkout/success');
cy.get('[data-testid="order-number"]').should('be.visible');
cy.get('[data-testid="order-confirmation-email-notice"]')
.should('contain.text', 'guest@example.com');
});
});
Login and registration deserve E2E coverage for a different reason: they're the entry point for practically every other journey, and a bug here blocks not just one feature area but the whole application for the affected users. It's important to cover both the guest and the registered login path, since Magento and Hyvä shops handle both flows differently, for instance around cart merging after login. Product search with filtering rounds out the trio, because it involves frontend state, URL parameters, and backend facet logic all at once. A single broken facet filter that returns an empty result list instead of an error message often goes unnoticed in unit tests, because facet combinations are rarely modeled realistically there.
4. Edge cases: why they rarely belong in the E2E suite
Edge cases like rounding errors in combined discounts, boundary values in volume discounts, or the correct handling of negative stock levels matter, but E2E tests are the wrong tool for them. The reason is combinatorics: testing ten discount types, three tax rates, and five currencies with a naive E2E approach would theoretically require hundreds of browser runs, each taking several seconds and needing a complete test environment. A unit test for the same price calculation function covers the same combinatorics in milliseconds, because it calls the function directly with synthetic inputs, no browser, network, or database involved.
// tests/unit/priceCalculator.spec.js
// Edge case: rounding when combining a percentage discount with tax
// Unit-level - no browser, no network, runs in milliseconds
import { calculateFinalPrice } from '../../src/pricing/priceCalculator';
describe('calculateFinalPrice - discount and tax rounding', () => {
it('rounds to two decimals after applying a percentage discount', () => {
const result = calculateFinalPrice({
basePrice: 19.99,
discountPercent: 15,
taxRate: 0.19,
});
// 19.99 * 0.85 = 16.9915 -> rounds to 16.99 before tax
expect(result.netPrice).toBe(16.99);
// 16.99 * 1.19 = 20.2181 -> rounds to 20.22
expect(result.grossPrice).toBe(20.22);
});
it('never produces a negative price when discount exceeds 100 percent', () => {
const result = calculateFinalPrice({
basePrice: 9.99,
discountPercent: 150,
taxRate: 0.19,
});
expect(result.netPrice).toBe(0);
expect(result.grossPrice).toBe(0);
});
it('handles stacked discounts without double-rounding errors', () => {
const result = calculateFinalPrice({
basePrice: 100,
discountPercent: 10,
secondDiscountPercent: 5,
taxRate: 0.19,
});
// 100 * 0.9 * 0.95 = 85.5, not 100 * 0.85 = 85
expect(result.netPrice).toBe(85.5);
});
});
The second reason is fault localization: when an E2E test fails because a discount was calculated incorrectly, the test report usually shows only that the final cart price is wrong, not which of the involved functions actually caused it. A unit test for that exact discount rule points straight to the exact line of code. In practice, a good rule of thumb is that every if-branch and every boundary value in pricing logic gets its own unit test, while the E2E suite plays through only a single representative checkout with a typical discount, to confirm the interplay as a whole.
5. The underestimated maintenance cost of E2E tests
The maintenance cost of an E2E suite is almost always underestimated in planning conversations, because it doesn't show up while writing the test, it shows up months later with every layout change, every A/B test, and every dependency update. A test built on a CSS selector like .btn-primary:nth-child(2) is guaranteed to break at the next redesign, regardless of whether the actual functionality changed at all. The fix is to consistently use stable, semantic selectors like data-testid attributes, which exist explicitly for tests and stay untouched by design changes.
// tests/e2e/login.spec.js (Playwright)
// Critical journey: registered customer login
// Uses stable data-testid selectors, not CSS classes tied to design
import { test, expect } from '@playwright/test';
test.describe('Customer login', () => {
test('logs in with valid credentials and merges the guest cart', async ({ page }) => {
// Seed a guest cart before login to verify the merge behavior
await page.goto('/catalog/product/MS-1234');
await page.getByTestId('add-to-cart-button').click();
await expect(page.getByTestId('minicart-count')).toHaveText('1');
await page.goto('/customer/account/login');
await page.getByTestId('login-email-input').fill('customer@example.com');
await page.getByTestId('login-password-input').fill('Test1234!');
await page.getByTestId('login-submit-button').click();
// Playwright auto-waits for navigation and visibility - no fixed sleeps
await expect(page).toHaveURL(/\/customer\/account\//);
await expect(page.getByTestId('welcome-message')).toContainText('customer@example.com');
// Guest cart item must survive the login and merge into the account cart
await expect(page.getByTestId('minicart-count')).toHaveText('1');
});
test('shows an inline error for invalid credentials without a page reload', async ({ page }) => {
await page.goto('/customer/account/login');
await page.getByTestId('login-email-input').fill('customer@example.com');
await page.getByTestId('login-password-input').fill('wrong-password');
await page.getByTestId('login-submit-button').click();
await expect(page.getByTestId('login-error-message')).toBeVisible();
await expect(page).toHaveURL(/\/customer\/account\/login/);
});
});
Flakiness, tests that pass or fail with no code change at all, is the second big maintenance trap. Common causes include hardcoded wait times instead of explicit wait conditions, race conditions between asynchronous API calls and UI updates, and test data that interferes across test runs. Playwright's automatic waiting for visibility and interactivity reduces this class of failure considerably compared to classic Selenium, but it doesn't remove the need to give every test isolated, reproducible test data, for example through API seeding instead of UI interaction for setup.
An often-overlooked cost factor is the CI runtime itself: a suite of a hundred E2E tests, each taking five to ten seconds, easily costs ten to fifteen minutes per build when run sequentially. Parallelizing across multiple CI runners reduces wall-clock time but increases infrastructure cost proportionally. Teams that don't consciously weigh these costs against the benefit risk either building a bloated, slow pipeline, or eventually disabling the suite entirely because nobody has time to maintain it anymore.
6. Decision criteria: what should NOT be E2E-tested
An explicit negative list is often more effective than a positive one, because it stops teams from adding ever more E2E tests out of caution. What should not be E2E-tested: pure field-level form validation, since it can be checked in isolation and much faster at the component level. Calculation logic with many branches, such as tax, discount, or shipping cost calculation, since the combinatorics become impractical in E2E tests. Rare failure paths like network timeouts or API error responses, which can be simulated far more reliably with mocks in unit and integration tests than in a real browser environment.
# e2e-journey-priorities.yml
# Scoring model: business_criticality, ui_change_frequency, unit_testability
# Each scored 1 (low) to 3 (high); only high-priority journeys enter the E2E suite
journeys:
- name: checkout_guest_purchase
business_criticality: 3
ui_change_frequency: 1
unit_testability: 1
decision: e2e
tag: "@critical"
- name: customer_login
business_criticality: 3
ui_change_frequency: 1
unit_testability: 1
decision: e2e
tag: "@critical"
- name: product_search_with_filters
business_criticality: 2
ui_change_frequency: 2
unit_testability: 1
decision: e2e
tag: "@smoke"
- name: discount_price_rounding
business_criticality: 2
ui_change_frequency: 1
unit_testability: 3
decision: unit
tag: "@unit-only"
- name: field_level_form_validation
business_criticality: 1
ui_change_frequency: 3
unit_testability: 3
decision: component
tag: "@component-only"
Purely visual aspects, like exact pixel spacing or gradients, also don't belong in functional E2E tests, they belong in dedicated visual regression tests with tools like Percy or Playwright's built-in screenshot comparison, which cover a different class of bugs entirely. A useful decision aid is a simple scoring model: business criticality, UI change frequency, and lower-tier testability are each scored from one to three. Only journeys with high criticality, low to medium change frequency, and poor unit-level testability end up in the E2E suite. Everything else is deliberately and explicitly excluded, not just forgotten.
7. Making the ROI case to stakeholders
Stakeholders without a technical background don't respond to test coverage percentages as an argument, but they do respond to avoided revenue loss and reduced incident costs. The most convincing ROI argument does the math concretely: if checkout processes an average of ten orders per minute, and an undetected bug takes checkout down for two hours before a customer reports it, that's twelve hundred lost orders. An E2E test that catches this bug in the CI pipeline before it goes live costs a few seconds of runtime and a few hours of initial development time. This comparison makes the investment tangible without requiring stakeholders to understand Cypress or Playwright at all.
A second compelling argument is the reduction in time-to-detection. Without E2E tests, a checkout bug is often first noticed through falling conversion rates in an analytics dashboard, which can take days, or through frustrated customer calls. With an E2E suite that runs automatically on every deployment, the same bug is caught within minutes, often before it ever reaches production. That time savings can be quantified in person-days of debugging and in avoided reputational damage, both of which are far more tangible for leadership and product management than an abstract metric like test coverage.
Honesty about the cost side matters for a credible ROI argument: a well-maintained E2E suite covering ten critical journeys realistically requires an estimated one to two person-days per month of maintenance, depending on how frequently the application changes. Teams that hide these costs and sell E2E tests as free insurance lose credibility for future testing investments the first time real maintenance work piles up. Presenting cost and benefit side by side, honestly, is more convincing in the long run than overblown promises.
8. Embedding E2E tests in the development process
E2E tests only deliver their full value once they're firmly embedded in the development process, rather than run as an afterthought check before a release. The most effective approach is categorizing tests by criticality with tags like @critical or @smoke, so that only the few critical journeys run on every pull request, while the full suite runs nightly or before a production deployment. This staging keeps the feedback loop short for developers without giving up the safety of a full regression run before release.
#!/usr/bin/env bash
# scripts/run-critical-e2e.sh
# Runs only specs tagged @critical on every pull request;
# the full suite runs on the nightly pipeline instead.
set -euo pipefail
readonly TAG="${1:-@critical}"
readonly SPEC_DIR="cypress/e2e"
echo "[INFO] Collecting specs tagged with ${TAG}"
mapfile -t critical_specs < <(grep -rl "${TAG}" "${SPEC_DIR}" --include="*.cy.js")
if [[ ${#critical_specs[@]} -eq 0 ]]; then
echo "[ERROR] No specs found for tag ${TAG}" >&2
exit 1
fi
echo "[INFO] Running ${#critical_specs[@]} critical spec(s)"
npx cypress run --spec "$(IFS=,; echo "${critical_specs[*]}")" --record --parallel
echo "[OK] Critical E2E suite passed"
A second important process element is ownership of red tests: an E2E test that fails must either be fixed or deliberately flagged as a known issue within hours, not days. Teams that let red tests sit for days lose trust in the whole suite, which leads to individual tests being quietly disabled and, eventually, to the entire investment losing its value. A clear owner per test suite and an escalation rule, for example that a red critical test blocks a merge, prevents this erosion.
New features should be designed from day one with the question of whether they represent a new critical journey or extend an existing one. That prevents E2E coverage from being bolted on months after launch, under time pressure, once an incident has already occurred. A short testing section in the pull request template that explicitly asks about the criticality of the changed journey makes this decision visible and traceable for the whole team, instead of leaving it to a single person.
9. Journey prioritization, side by side
Not every journey deserves the same test depth, and the differences can be clearly demonstrated with a handful of examples. The following overview compares typical e-commerce scenarios and shows where E2E tests give the most leverage and where unit or component tests are the better choice.
| Scenario | Worth E2E testing? | Recommended tier | Reasoning |
|---|---|---|---|
| Completing checkout | Yes | E2E (Cypress/Playwright) | Combines cart, payment, stock check, and confirmation |
| Price rounding on discounts | No | Unit test | High combinatorics, isolated calculation logic |
| Login with valid credentials | Yes | E2E (Cypress/Playwright) | Entry point for every other journey |
| Field-level form validation | No | Component test | Checkable in isolation, no cross-system interplay needed |
| Product search with filters | Yes | E2E (Cypress/Playwright) | Combines frontend state, URL, and backend facets |
| Cart merge after login | Yes | E2E (Cypress/Playwright) | Rare but business-critical system transition |
| Shipping cost calculation edge cases | No | Unit test | Many boundary values, isolated function |
A clear pattern emerges: journeys that couple multiple systems, where a failure directly costs revenue or trust, belong in the E2E suite. Journeys that reduce to a single, well-isolated function are cheaper and more reliably secured at a lower test tier. Run your own application through this table as a template, and you'll typically land on an E2E suite of five to ten journeys, instead of hundreds of individual tests buying the same level of confidence at a much higher price.
Mironsoft
E2E test strategy, Cypress/Playwright suites, and test coverage consulting
E2E tests that actually reduce risk instead of eating up time?
We analyze your critical user journeys, evaluate your existing Cypress or Playwright suite, and prioritize test coverage by business risk instead of gut feeling, with clear criteria for what does NOT need to be E2E-tested.
User journey audit
Identify critical journeys and prioritize by revenue and risk impact
Cypress/Playwright suite review
Check flakiness, maintenance cost, and selector stability of existing suites
Test coverage prioritization
Establish a decision matrix for E2E vs. unit vs. component testing across your team
10. Summary
Deciding when E2E tests are worthwhile isn't about hitting a blanket coverage quota, it's about a clear risk assessment: checkout, login, and product search with filtering deserve end-to-end coverage because they couple multiple systems and directly cost revenue or trust if they fail. Edge cases like combined discounts or boundary values in price calculation, on the other hand, belong at the unit level, where they're checked faster, cheaper, and with more precise fault localization. This separation keeps the E2E suite to a manageable, maintainable number of critical journeys instead of hundreds of slow, fragile tests.
The maintenance cost of an E2E suite is real and needs to be communicated honestly to stakeholders, but it can be significantly reduced through stable selectors, consistent criticality-based categorization, and clear ownership. Teams that embed E2E tests firmly in the development process, instead of treating them as an afterthought check, gain a safety layer that genuinely builds confidence, rather than becoming an ignored source of noise in the CI pipeline.
When E2E Tests Make Sense and When They Hurt - Key Takeaways
Critical journeys first
Checkout, login, and search deserve E2E coverage because they couple multiple systems and directly affect revenue.
Edge cases at the unit level
Price rounding, combined discounts, and boundary values belong in fast, isolated unit tests, not the E2E suite.
Plan for maintenance
Stable data-testid selectors, isolated test data, and clear ownership reduce flakiness and upkeep.
Quantify ROI concretely
Lost orders per hour of downtime and reduced time-to-detection convince stakeholders far more effectively than coverage percentages.