The checkout paths your test suite misses

The suite is green, and customers still cannot pay. Seven journeys that do not get tested, and how to test them.

Almost every checkout suite I have read tests the same journey: an empty basket, one in-stock product, a new guest, a test card that always succeeds, and an assertion on the words "thank you for your order". That journey is worth testing. It is also the one journey that almost never breaks.

Why a green suite still lets checkout bugs through

Three structural reasons, and none of them are about writing more tests.

The suite asserts on the DOM, and the failure is in the state. A browser test checks what the page says. An order is a row in a database, a payment is a record at a gateway, and stock is a number that moved. A page can say "thank you for your order" while the order is in the wrong state, the payment captured nothing, and the stock never decremented.

The suite runs one customer. One fixture user, one address, one card, one basket, on a database that is either freshly seeded or quietly accumulating state from previous runs. Real checkouts fail on the second visit, the saved address, the expired coupon and the basket left over from three weeks ago.

The suite stops where the money starts. The interesting half of a payment happens after the browser leaves: the gateway redirect, the asynchronous notification, the signature check, the order transition. Testing usually stops at the redirect because that is where the browser automation gets awkward, which is exactly why that code is the least exercised in the whole application.

What follows is the list I work through when I review or build a suite for a store. Each one is a journey that breaks in production, with the reason the suite does not see it and the way to test it.

1. The returning customer, not the new one

What breaks: cart merge on login. A customer adds two items as a guest, then signs in at checkout. Depending on the platform and its configuration, they now have the two items, four items, or an empty basket with an orphaned order sitting in the database. Drupal Commerce, WooCommerce and osCommerce all have their own version of this, and it is usually configurable, which means it is usually untested.

Why it is missed: the suite either tests guest checkout or tests logged-in checkout. The bug lives in the transition between them.

How to test it: one scenario that adds to basket anonymously, signs in mid-checkout, and asserts on the exact basket contents and line totals, not just on the page loading. Add a second scenario where the account already has an abandoned basket from a previous session, because merge plus merge is where the double-quantity bugs live.

2. Stock that changes while the customer is in checkout

What breaks: the last unit is sold between "add to basket" and "pay". Good implementations refuse the order or reduce the line. Bad ones take the money and oversell, which is a customer service problem rather than a 500 error, so nothing in your monitoring notices.

Why it is missed: tests run against a fixture with plenty of stock, and each test owns its own basket. Nothing ever competes for the last unit.

How to test it: mid-test, change the stock behind the browser's back. Drop the quantity to zero with a direct API or database call after the basket is built and before payment, then assert on what the customer is told. In Playwright this is just an await on your own fixture helper between two page interactions. The valuable assertion is not that an error appears, it is which error appears and whether the payment was attempted.

3. Baskets with mixed tax and awkward delivery

What breaks: a basket containing a zero-rated item and a standard-rated item, with a shipping charge that has to be apportioned between them. Or a UK store shipping to the Highlands, the Channel Islands, or Northern Ireland, each of which has its own rules and its own surcharge table.

Why it is missed: the fixture basket holds one product and ships to a mainland postcode. Tax code paths only diverge when the basket is mixed.

How to test it: assert on numbers, not on the presence of a totals block. Three or four table-driven scenarios, each with an expected subtotal, tax and grand total, will find more real bugs than another twenty navigation tests. This is also the cheapest place to catch a rounding bug, which is the class of defect that quietly generates accounting queries for years.

4. Discounts at their boundaries

What breaks: free shipping over a threshold, combined with a percentage coupon that takes the basket back below it. Two coupons that should not stack. A code that expired at midnight. A minimum-spend rule evaluated before the customer removes a line rather than after.

Why it is missed: the suite has one coupon and it always applies cleanly.

How to test it: test the boundary, not the middle. One penny below the threshold and one penny above it. An expired code, checked with the clock moved rather than by waiting: Playwright can override the page clock, and any date-dependent rule evaluated server-side needs a seeded expiry date instead. Then the removal case, which is the one that actually costs money: qualify for the discount, remove a line, and assert the discount was recalculated.

5. Payments that fail on purpose

What breaks: the decline path. A declined card should leave the customer on the payment step with a usable message and no order, no confirmation email, and no decremented stock. What often happens instead is an order stuck in a half-created state, or a message that reads like a stack trace.

Why it is missed: the suite uses the gateway's always-succeeds test card, because that is the card in the documentation.

How to test it: every serious gateway publishes card numbers or amounts that force specific outcomes: decline, insufficient funds, expired card, a 3-D Secure challenge. Use them. Where the gateway cannot be driven into a state you need, intercept the request in the browser instead. Playwright's page.route() will let you fulfil a gateway call with a canned failure response and assert on what your own code does with it. That is not testing the gateway, which is not your job. It is testing your error handling, which is.

Three more decline-adjacent cases worth a scenario each: the customer who presses browser back after paying, the customer who double-submits the pay button, and the customer who abandons the gateway redirect and returns to the site later. All three are ordinary human behaviour and all three are capable of producing a duplicate order.

6. The webhook nobody tests

This is the big one, and it is the reason I put payment integration work in a different category to the rest of a build.

What breaks: on a hosted-payment or redirect integration, the browser is not the authority on whether the order was paid. The gateway's server-to-server notification is. If that endpoint is wrong, the customer sees a confirmation page and your order never transitions to paid, or the reverse: the payment succeeds, the customer closes the tab on the gateway's page, and the order is only ever completed by the notification that your suite has never once exercised.

Why it is missed: a browser-driven test cannot easily make a third party post to your server, so the endpoint is tested by hand once, during the build, and then never again. It is quite common to find that it has been broken for months and nobody noticed, because most customers do wait for the redirect.

How to test it: stop trying to do it through the browser. Post to the endpoint directly from the test with a correctly signed payload, exactly as the gateway would, and assert on the resulting order state. Then test the cases that matter more than the happy one:

These are four fast tests with no browser involved, and between them they cover the code path that moves the money.

7. The confirmation email and everything downstream

What breaks: the order is fine and the customer hears nothing, because the mail transport silently failed, the template references a token that no longer exists, or the mail went out with a blank total. Downstream of that: the ERP export, the accounting sync, the fulfilment feed.

Why it is missed: mail is disabled in the test environment, which is sensible, and so nothing asserts that mail was even attempted.

How to test it: capture rather than send. A catching mail transport in the test environment, then assert the message exists, is addressed to the right customer, and contains the order number and total. It is a small test that covers a failure customers always report and monitoring never catches.

How to find out whether your own suite is blind

Everything above is a list of what to add. The prior question is how much your current suite is really checking, and there is a measurement for that rather than an opinion: fault injection. Insert a known fault, run the suite, record whether it noticed, then repeat with different fault types.

I ran exactly that exercise on a production Drupal site, injecting ten faults across the custom modules and theme.

Five of the ten faults went completely undetected. The suite caught every change to the HTML and CSS it asserted on, and was blind to every JavaScript fault, including a fatal error in a shared script that killed every behaviour on every page. A broken AJAX add-to-cart command produced zero failures.

The fix was two small helpers rather than a rewrite: a pageerror listener that fails a test on any uncaught JavaScript exception, and a response listener that fails on a 4xx or 5xx for a JavaScript or CSS asset. Detection went from 50% to 70%, and the remaining gaps were authenticated flows outside the suite's scope. The full table, fault by fault, is in the site quality audit case study.

For a store, those two listeners matter more than they do anywhere else, because a checkout that fails silently in JavaScript looks exactly like a checkout that works right up until the customer tries to pay.

If you only have a day

Assume an existing suite that covers browsing and the happy-path checkout. In one day, in this order:

  1. Add the pageerror and failed-asset listeners to the shared fixture. Nothing else here gives you as much coverage per line.
  2. Write the four webhook tests. No browser, fast, and they cover the code that moves the money.
  3. Write the cart-merge scenario and the stock-changed-mid-checkout scenario.
  4. Write three table-driven tax and shipping assertions with real expected totals.
  5. Write the declined-card scenario and assert that no order and no email were created.

That is roughly a dozen scenarios. It will find more than the next hundred navigation assertions, because every one of them sits on a code path where state changes rather than where markup changes.

The rule underneath all of it

Test where the state changes, not where the DOM changes. Almost every miss in this article comes from the same habit: writing tests that follow what a person looks at, rather than what the application writes down. A customer looks at a confirmation page. The business cares about an order row, a payment record, a stock level and an email. Those are four different assertions, and the page tells you about none of them.

The corollary is that a good e-commerce suite is not all browser tests. It is a small number of browser journeys for the paths a human really walks, plus a set of fast, headless tests against the endpoints and state transitions underneath. Suites that are 100% browser tests are slow, flaky, and blind in exactly the places that cost money.

If you want this done properly

I build and maintain Playwright suites for PHP e-commerce stores, including Drupal Commerce, WooCommerce, osCommerce and bespoke PHP, as a fixed-price e-commerce testing engagement: the critical journeys agreed up front, the suite delivered and documented, and scheduled runs with failure alerts if you want the store watched rather than just tested once.

If you would rather know how bad it is before committing to anything, the fault injection measurement is part of a fixed-price site audit, and agencies commission both white-label under Drupal for agencies.

© 2026 Graith Internet.

Graith Internet is a UK web development company specialising in PHP, Drupal, and AI-assisted development.

Find this useful? Tell Google to show you more from Graith Internet in Search, Discover and News.