How to Test Checkout Process on Web (Complete Guide)

A checkout flow is the final gate where a visitor decides whether to become a paying customer. If any step fails—whether a button does not respond, a form validation error blocks progress, or a securi

May 06, 2026 · 17 min read · How-To Guides

Why Checkout Testing Matters

A checkout flow is the final gate where a visitor decides whether to become a paying customer. If any step fails—whether a button does not respond, a form validation error blocks progress, or a security warning appears—users abandon the cart and revenue leaks instantly. Studies show that a single second of extra latency can cut conversion by up to 7 %, and a broken checkout can cause double‑digit losses in a single day. Beyond revenue, checkout defects damage brand trust, increase support load, and may expose personal data if validation or encryption is mishandled. Because the flow touches UI, business logic, third‑party APIs, and often legacy code paths, it is a hotspot for regressions after any feature release, refactor, or dependency upgrade. Testing it thoroughly therefore protects both the bottom line and the user experience.

Test Matrix for Checkout Process

Test IDCategoryDescriptionExpected ResultPriority
C1Happy PathUser adds item to cart, proceeds to checkout, enters valid shipping & billing info, selects a saved card, confirms orderOrder confirmation page shows order number, email receipt sent, inventory decrementedP0
C2Happy Path – GuestSame as C1 but user checks out as guest (no account creation)Order placed, guest receives email with order details, no account createdP0
C3Happy Path – Multiple Shipping AddressesUser ships items to two different addresses in one orderSystem splits shipment, shows two tracking numbers, charges correct shipping per addressP1
C4Error Path – Invalid Card NumberUser enters a card number that fails Luhn checkInline validation shows “Invalid card number”, submit button stays disabledP0
C5Error Path – Expired CardUser enters a card with past expiry dateError message “Card has expired”, focus moves to expiry fieldP0
C6Error Path – Missing Required FieldUser leaves shipping zip code empty and tries to continueValidation highlights zip field, tooltip “Zip code is required”P0
C7Error Path – Shipping Address Outside Service AreaUser enters a zip code not served by shipping carrierCarrier API returns unavailable, UI shows “We cannot ship to this address” with suggestion to editP1
C8Edge Case – Cart EmptyUser navigates directly to checkout URL with empty cartRedirect to cart page with message “Your cart is empty”P1
C9Edge Case – Quantity ZeroUser updates line‑item quantity to zero before checkoutItem removed from cart, cart total updates, checkout button disabled if cart emptyP1
C10Edge Case – Applied Coupon Exceeds TotalUser applies a coupon that gives discount greater than order subtotalSystem caps discount at order total, shows final amount $0.00, does not allow negative totalP1
C11Edge Case – Concurrent ModificationTwo tabs open: user changes quantity in Tab A, proceeds to checkout in Tab BCheckout reflects latest cart state (quantity from Tab A) or shows warning “Cart changed, please review”P2
C12Accessibility – Keyboard NavigationUser tabs through all form fields, buttons, and links without mouseFocus order is logical, visible focus indicator, all controls operable via Enter/SpaceP0
C13Accessibility – Screen Reader LabelsUser navigates with NVDA or VoiceOverEvery form field has associated P0
C14Accessibility – Color ContrastUser views page with high‑contrast mode or forced colorsText and UI elements meet WCAG AA contrast ratio (≥4.5:1 for normal text)P1
C15Security – SQL Injection AttemptUser inputs ' OR 1'='1-- in coupon code fieldInput sanitized, no database error, validation shows “Invalid coupon”P0
C16Security – XSS via Shipping AddressUser enters in address line 2Script is escaped or stripped, no execution in DOM, address displayed as plain textP0
C17Security – Token LeakageAfter successful payment, inspect network calls for accidental exposure of payment token in URL or logsToken appears only in POST body, never in query string, response headers, or console logsP0
C18Privacy – GDPR ConsentUser from EU region checks out without accepting optional marketing consentOrder proceeds, but marketing opt‑in checkbox remains unchecked, no tracking cookies set for marketingP1
C19Performance – Slow Third‑Party GatewaySimulate 2‑second delay in payment gateway responseCheckout shows spinner, does not timeout, user can cancel after 10 s, error handling gracefulP2
C20Locale – Right‑to‑Left LanguageUser switches UI to Arabic or HebrewLayout mirrors, form fields align correctly, no overlapping elementsP2

*Priority*: P0 = blocker for release, P1 = high, P2 = medium.

Tooling Comparison for Automated Web Checkout Tests

ToolLanguage SupportBuilt‑in WaitsMocking / Network InterceptionVisual TestingCI‑FriendlinessLearning Curve
PlaywrightTypeScript/JavaScript, Python, .NET, JavaAuto‑wait for actionability, network idlepage.route() for request/response mockingexpect(page).toHaveScreenshot()Excellent (Docker images, GitHub Actions)Low‑moderate
CypressJavaScript/TypeScriptAutomatic retry, cy.wait()cy.intercept() for stubbingThird‑party plugins (cypress-image-snapshot)Good (cypress dashboard)Low
Selenium WebDriverJava, C#, Python, Ruby, JSExplicit/WebDriverWait neededRequires external tools (WireMock, MockServer)Requires external frameworks (Applitools, Percy)Good (Selenium Grid)Moderate‑high
TestCafeJavaScript/TypeScriptSmart assertions, auto‑waitt.setRequestHook() for mockingPlugin availableGood (no WebDriver)Low

Manual Testing Approach

Preparation

  1. Environment snapshot – Record the exact version of the front‑end bundle, backend API, and any feature flags affecting checkout (e.g., new payment method toggle).
  2. Test data – Prepare a spreadsheet with valid and invalid card numbers (using test suites from Stripe, Braintree, or Adyen), address variations (domestic, international, PO boxes), coupon codes, and user personas (guest, logged‑in, new vs returning).
  3. Tooling – Have a browser with developer tools open, a network throttling profile (e.g., “Slow 3G”), and a screen‑reader extension (NVDA, VoiceOver, or ChromeVox).
  4. Session capture – Use a plugin like “SessionBuddy” or the browser’s built‑in history to be able to revert to a clean cart state between tests.

Step‑by‑step Walkthrough (Happy Path)

  1. Load the product catalog page, add two different SKUs to the cart. Verify the cart badge updates.
  2. Click the cart icon, review line items, quantities, and subtotal. Ensure “Proceed to checkout” is enabled.
  3. On the checkout landing page, choose “Checkout as guest”. Confirm that no account‑creation fields appear.
  4. Fill shipping address: use a valid domestic address, then repeat with an international address that requires a customs declaration field. Verify that address‑validation service (if present) returns correct suggestions.
  5. Move to billing section. If the site offers “Use shipping address as billing”, test both toggling on and off.
  6. Enter a valid test card number (e.g., 4242 4242 4242 4242 for Stripe), future expiry, and correct CVC. The submit button should become active only after all fields pass client‑side validation.
  7. Click “Place order”. Observe a loading spinner, then a redirect to the order‑confirmation page. Verify the order number format matches expectations (e.g., ORD-2025-000123).
  8. Check email inbox (use a disposable mailbox like Mailinator) for the order confirmation. Ensure the email contains the correct items, totals, and a link to view order status.
  9. In the admin/order‑management UI, confirm that inventory decreased by the ordered quantities and that payment status is “captured”.

Session Recording and Notes

Exploratory Testing Tips

Automated Testing Approaches

Choosing the Framework

For a modern SPA, Playwright offers the best out‑of‑the‑box handling of network idle states and built‑in tracing. If the team already uses Cypress for other UI tests, extending it to checkout is straightforward because of its familiar command syntax. Selenium remains an option when cross‑browser testing on legacy IE/Edge is required, though it demands more explicit waits.

Setting up the Test Environment

  1. Isolated test tenant – Provision a separate environment (e.g., checkout-test.susatest.com) with its own database snapshot and feature flags disabled for experiments.
  2. Test data seeding – Before each test suite run, call an API endpoint /test/reset that clears orders, resets inventory, and populates a set of known products.
  3. Mock payment gateway – Use a tool like MSW (Mock Service Worker) or WireMock to intercept calls to the payment provider endpoint and return predefined success/failure responses. This removes reliance on external sandboxes and makes tests deterministic.
  4. Configure CI – Add a Docker‑based step that runs npm ci && npx playwright test (or cypress run). Upload Playwright traces or Cypress videos as artifacts for failed runs.

Writing a Happy‑Path Test (Playwright + TypeScript)


// tests/checkout.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Checkout happy path', () => {
  test.beforeEach(async ({ page }) => {
    // Reset state via API
    await page.request.post('/test/reset');
    // Seed cart via API (skip UI for speed)
    await page.request.post('/cart/add', {
      data: { sku: 'SKU-001', qty: 1 }
    });
    await page.request.post('/cart/add', {
      data: { sku: 'SKU-002', qty: 2 }
    });
    await page.goto('/cart');
  });

  test('guest checkout with card', async ({ page }) => {
    // Go to checkout
    await page.click('text=Proceed to checkout');
    await expect(page).toHaveURL(/\/checkout\/?/);

    // Choose guest
    await page.check('input#checkout-as-guest');

    // Fill shipping
    await page.fill('input[name="shipping_first_name"]', 'Ada');
    await page.fill('input[name="shipping_last_name"]', 'Lovelace');
    await page.fill('input[name="shipping_line1"]', '123 Science St');
    await page.fill('input[name="shipping_city"]', 'London');
    await page.selectOption('select[name="shipping_country"]', 'GB');
    await page.fill('input[name="shipping_postcode"]', 'SW1A 1AA');
    await page.fill('input[name="shipping_phone"]', '+44 20 7946 0958');

    // Continue to billing
    await page.click('button:has-text("Continue to billing")');

    // Use shipping as billing
    await page.check('input#use-shipping-as-billing');

    // Payment details – using Stripe test card
    await page.fill('input[name="card_number"]', '4242 4242 4242 4242');
    await page.fill('input[name="card_expiry"]', '12/34');
    await page.fill('input[name="card_cvc"]', '123');

    // Submit order
    await page.click('button:has-text("Place order")');

    // Wait for confirmation
    await expect(page.locator('h1')).toContainText('Order confirmed');
    const orderNumber = await page.textContent('span.order-number');
    expect(orderNumber).toMatch(/ORD-\d{4}-\d{5}/);

    // Verify email via a test mailbox API (example)
    const mailResponse = await page.request.get(
      `https://api.mailbox.test/messages?recipient=ada%40example.com&subject=Order%20confirmed`
    );
    const mailJson = await mailResponse.json();
    expect(mailJson.length).toBeGreaterThan(0);
    expect(mailJson[0].body).toContain(orderNumber);
  });
});

Explanation of key lines

Parameterizing Data for Error Paths

Create a JSON fixture checkoutData.json:


[
  { "description": "Invalid card number", "cardNumber": "4242 4242 4242 4241", "expectError": true, "errorMsg": "Invalid card number" },
  { "description": "Expired card", "cardNumber": "4242 4242 4242 4242", "expiry": "01/20", "expectError": true, "errorMsg": "Card has expired" },
  { "description": "Missing ZIP", "postcode": "", "expectError": true, "errorMsg": "Zip code is required" }
]

Then drive the test:


import testData from '../fixtures/checkoutData.json';

testData.forEach(({ description, cardNumber, expiry, postcode, expectError, errorMsg }) => {
  test(`Error path: ${description}`, async ({ page }) => {
    // …setup as before…
    await page.fill('input[name="card_number"]', cardNumber ?? '');
    if (expiry) await page.fill('input[name="card_expiry"]', expiry);
    if (postcode) await page.fill('input[name="shipping_postcode"]', postcode);
    await page.click('button:has-text("Place order")');

    if (expectError) {
      const err = page.locator('.field-error', { hasText: errorMsg });
      await expect(err).toBeVisible();
      await expect(page.locator('button:has-text("Place order")')).toBeDisabled();
    } else {
      await expect(page.locator('h1')).toContainText('Order confirmed');
    }
  });
});

Handling Async Waits and Network Idle

Playwright automatically waits for elements to become actionable (visible, enabled, stable). For custom scenarios—like waiting for a fraud‑check service to respond—use:


await page.waitForResponse(resp => resp.url().includes('/fraud-check') && resp.status() === 200);

If you need to assert that no further network activity occurs after a certain point:


await page.waitForLoadState('networkidle');

Mocking Payment Gateways

Using MSW (Node):


// mocks/handlers.js
import { rest } from 'msw';

export const handlers = [
  rest.post('https://api.stripe.com/v1/payment_intents', (req, res, ctx) => {
    // Simulate success
    return res(
      ctx.status(200),
      ctx.json({
        id: 'pi_1Fake',
        status: 'succeeded',
        amount_received: 2500,
        currency: 'usd'
      })
    );
  })
];

In Playwright’s beforeAll, start the worker:


import { setupWorker } from 'msw';
import { handlers } from '../mocks/handlers';

let worker;

test.beforeAll(async () => {
  worker = setupWorker(...handlers);
  await worker.start();
});

test.afterAll(async () => {
  await worker.stop();
});

This approach lets you simulate declines, timeouts, or malformed responses without touching a real sandbox.

CI Integration Example (GitHub Actions)


name: Checkout Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: checkout_test
        ports: [5432:5432]
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Install deps
        run: npm ci
      - name: Start mock services
        run: npx msb start   # hypothetical wrapper for MSW/WireMock
      - name: Run Playwright
        run: npx playwright test --project=chromium
      - name: Upload traces
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-traces
          path: playwright-trace/*

Code Examples – Additional Snippets

Cypress Version (JavaScript)


// cypress/integration/checkout_spec.js
describe('Guest checkout', () => {
  beforeEach(() => {
    cy.request('POST', '/test/reset');
    cy.request('POST', '/cart/add', { sku: 'SKU-001', qty: 1 });
    cy.request('POST', '/cart/add', { sku: 'SKU-002', qty: 2 });
    cy.visit('/cart');
  });

  it('places an order with a test card', () => {
    cy.contains('Proceed to checkout').click();
    cy.get('#checkout-as-guest').check();

    cy.get('input[name="shipping_first_name"]').type('Ada');
    cy.get('input[name="shipping_last_name"]').type('Lovelace');
    cy.get('input[name="shipping_line1"]').type('123 Science St');
    cy.get('input[name="shipping_city"]').type('London');
    cy.get('select[name="shipping_country"]').select('GB');
    cy.get('input[name="shipping_postcode"]').type('SW1A 1AA');
    cy.get('input[name="shipping_phone"]').type('+44 20 7946 0958');

    cy.contains('Continue to billing').click();
    cy.get('#use-shipping-as-billing').check();

    cy.get('input[name="card_number"]').type('4242 4242 4242 4242');
    cy.get('input[name="card_expiry"]').type('12/34');
    cy.get('input[name="card_cvc"]').type('123');

    cy.contains('Place order').click();

    cy.get('h1').should('contain.text', 'Order confirmed');
    cy.get('span.order-number').should('match', /ORD-\d{4}-\d{5}/);
  });
});

Using Environment Variables for Test Cards

Store test card numbers in .env.test (never commit real PANs):


TEST_CARD_NUMBER=4242424242424242
TEST_CARD_EXPIRY=12/34
TEST_CARD_CVC=123

In Playwright:


const cardNumber = process.env.TEST_CARD_NUMBER?.replace(/(....)/g, '$1 ') ?? '';
await page.fill('input[name="card_number"]', cardNumber);

Dockerized MSW Mock for CI


# Dockerfile.mock
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY mocks/ ./mocks/
COPY start-mock.sh .
CMD ["sh", "start-mock.sh"]

start-mock.sh:


#!/set -e
npx cross-env MSW_DEBUG=1 node ./mocks/server.js &
wait

Autonomous, Persona‑Driven Exploration with SUSA

How SUSA Works

SUSA launches a headless Chromium instance, loads the target URL, and then lets a set of persona agents act on the page. Each persona is driven by a behavior model that defines click‑through speed, tolerance for errors, propensity to explore alternative UI elements, and likelihood to use assistive technology. As the agents interact, SUSA records every DOM mutation, network request, console error, and accessibility violation. It builds a graph of visited screens and dead‑ends, then uses that knowledge on subsequent runs to prioritize unexplored paths.

Persona Profiles Relevant to Checkout

PersonaKey TraitsTypical Checkout Actions
CuriousHigh exploration, tries every link and buttonClicks on “Learn more about shipping”, opens modal for gift‑wrap options, attempts to edit cart from checkout page
ImpatientLow tolerance for delays, aborts if spinner > 2 sMay refresh page, try to bypass steps by directly navigating to /checkout/complete
NoviceRelies on labels, avoids icons, prefers defaultsOften misses optional fields, may leave coupon box blank, needs clear error messages
AdversarialActively tries to break the systemEnters SQL injection strings, extremely long inputs, attempts to tamper with hidden fields via DevTools
ElderlyLarger tap targets, prefers high contrast, may use zoomUses browser zoom 150 %, relies on visible focus outlines, may mis‑click small icons
AccessibilityUses screen reader, keyboard onlyNavigates via Tab, expects live region announcements, looks for aria‑labels on icons
Power UserUses shortcuts, autofill, prefers saved payment methodsClicks browser autofill, attempts to apply multiple coupons, uses keyboard shortcuts to jump between fields

What It Finds That Scripts Never Look For

  1. Hidden navigation traps – A “Continue” button that is visually hidden but still focusable, causing keyboard users to get stuck. Scripts that rely on visible selectors miss this because they query by visible text.
  2. Dynamic coupon validation latency – When a coupon triggers a backend promo‑code check that takes >3 s under load, the impatient persona may abandon, while a script with a fixed timeout would either fail incorrectly or never notice the delay.
  3. Accessibility live‑region gaps – An error message injected into a
    without aria-live="assertive"; screen‑reader users never hear it. Automated assertions that only check DOM presence won’t catch the missing live attribute unless explicitly coded.
  4. Cross‑origin iframe interference – A third‑party fraud‑check widget loads in an iframe and steals focus; the power‑user persona attempts to use Cmd+L to focus the address bar and discovers the iframe blocks keyboard escape.
  5. Locale‑specific formatting bugs – When the site switches to Japanese Yen, the price field shows a half‑width character that breaks the regex used for price parsing in automated tests, but the curious persona notices the mis‑aligned total.
  6. Ad‑blocker induced element removal – A popular ad‑blocker hides a promotional banner that also contains a required script for address autocomplete; the novice persona can’t fill the address because the widget never appears, while a test running with a clean profile passes.

Example Output (Condensed)


[2025-09-26 10:14:23] Persona: Impatient
 - Action: Navigated directly to /checkout/complete after 1.2s spinner
 - Result: HTTP 403, session expired, redirected to login
 - Observation: No client‑side warning shown; user sees blank page

[2025-09-26 10:15:07] Persona: Accessibility
 - Action: Tabbed through payment form, focused on CVC input
 - Result: No aria-describedby attached to CVC field
 - Violation: WCAG 2.1 1.3.1 (Info and Relationships)

[2025-09-26 10:16:41] Persona: Adversarial
 - Action: Pasted 5000‑character string into coupon field
 - Result: Backend returned 500 Internal Server Error, stack trace leaked in response body
 - Security finding: Potential information disclosure

[2025-09-26 10:18:09] Persona: Elderly (zoom 150%)
 - Action: Attempted to click “Edit shipping” icon (12 px)
 - Result: Missed click, hit adjacent “Apply coupon” button
 - UI issue: Touch target too small per WCAG 2.5.5

These findings illustrate why a purely scripted suite can miss subtle UX, accessibility, and security defects that only manifest under realistic, varied human behavior.

Production‑Only Edge Cases

Network Flakiness

Even with a perfect test suite, transient DNS spikes or intermittent packet loss can cause a payment gateway request to time out. In production you may see a surge of “Gateway timeout” errors that never appear in staging because the test environment sits behind a reliable corporate ISP. Mitigation: implement retry with exponential backoff at the service‑layer level, and surface a user‑friendly “Please try again later” message after the second attempt.

Third‑Party Service Latency

Payment processors occasionally experience slowdowns during peak shopping periods (e.g., Black Friday). A checkout that assumes a sub‑second response will show a blank spinner indefinitely if the timeout is set too high or not at all. Use feature flags to dynamically adjust timeout thresholds based on real‑time latency metrics from the gateway’s status API.

Browser Extensions Interfering

Extensions like Honey, Rakuten, or password managers often inject iframes or modify form fields. An extension might auto‑fill a credit‑card number into a hidden field, causing a duplicate submission or a validation conflict. In production you’ll observe a small but steady percentage of orders with mismatched card‑holder name vs. billing address. Countermeasure: sanitize inputs on the server, ignore fields not present in the original DOM, and consider a CSP that restricts unauthorized inline scripts.

Locale and Currency Edge Cases

A shop serving multiple regions may switch currency symbols based on IP geolocation. If the front‑end assumes a fixed decimal separator (.) but the locale uses a comma (,), the parsed amount can be off by a factor of 100, leading to either under‑charging or over‑charging. The bug only shows up when the geo‑service returns a specific country code; tests that hardcode en-US will never catch it. Solution: rely on the ICU/intl library for number formatting and validate amounts server‑side against the cart total expressed in the shop’s base currency.

A/B Test Variations

Experiments that modify the checkout flow (e.g., a new address‑autocomplete widget) may be enabled for only 5 % of users. If your test suite runs against the stable branch, you never see the variant. Production monitoring should therefore include feature‑flag awareness: log the flag version with each checkout attempt and alert if conversion deviates significantly for a particular flag state.

Dark Mode and Forced Colors

Users who enable forced colors mode (Windows high contrast) or prefer dark mode may see insufficient contrast on placeholder text or inactive buttons. Automated accessibility checks that run in the default light theme miss these violations. Run your axe‑core or similar scans with the prefers-color-scheme: dark and forced-colors: active media queries emulated via Playwright’s page.emulateMedia({ colorScheme: 'dark', forcedColors: 'active' }).

Checklist for Release

PhaseItemHow to Verify
Pre‑releaseSmoke test happy path on staging with real test card (via sandbox)Manual or automated; confirm order confirmation email received
Run full test matrix (error paths, edge cases) in CIAll P0/P1 tests pass; P2 failures investigated
Accessibility audit (axe-core) with default, dark, forced‑colorsNo WCAG AA violations
Security scan (OWASP ZAP passive) on checkout endpointsNo high/medium findings (e.g., no SQLi, XSS)
Performance budget: checkout page load < 2 s on 3G simulatedLighthouse performance score ≥ 90
Feature‑flag verification: all toggles off for release candidateConfirm via config API
Post‑releaseMonitor conversion funnel in analytics for drop‑off >5 % vs baselineAlert on anomaly
Track payment gateway error rates (timeouts, declines)SLA < 1 %
Watch for console errors in real‑user monitoring (RUM)New JS errors trigger investigation
Review logs for leaked sensitive data (e.g., card numbers in query strings)Automated grep on log aggregates
Conduct a 15‑minute exploratory session with a novice user personaNote any hesitation or confusion

If any item fails, the release should be blocked until remedied and re‑checked.

Takeaways

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free