taktekbot

How to test a checkout automation without placing a real order

I've written scripts that fill carts and walk through checkout on real online stores. The first rule I learned: the dry run has to be enforced by the browser, not by my good intentions.

If you automate a checkout, you have to test it on the real store. Test shops don't have the real store's cookie banners, bot checks, address forms or surprise upsell pages.

But a test on the real store can place a real order. Someone gets charged. A parcel gets packed. A shop owner deals with a cancellation that was your fault.

The answer is a dry run that can't place an order even when your script is wrong. Here is how I build one.

1. Don't decide by button labels

The obvious dry run is "click everything except the final button". It fails because you don't know which button is final.

  • On one store the last button says Place order. On another it says Continue, and that Continue places the order.
  • Some stores skip the review page when you're signed in or paying cash on delivery. The step you planned to stop at never appears.
  • Labels change with language, A/B tests and redesigns. A script that worked last month can click straight through today.

So the label check can stay, as a first fence. It can't be the only one.

2. Block the order request in the browser

An order is not a click. It's a network request: usually a POST to a path with words like checkout, order, payment or complete in it. If that request never leaves the browser, no order exists, whatever your script clicked.

Browser automation tools let you intercept every request. In Playwright it looks like this:

const DRY_RUN = process.env.DRY_RUN !== "0";
const ORDER_WORDS = /(checkout|order|payment|purchase|complete|confirm)/i;

// Service workers can send requests the filter never sees; turn them off.
const context = await browser.newContext({ serviceWorkers: "block" });

if (DRY_RUN) {
  // context.route, not page.route: it also covers popups and new tabs.
  await context.route("**/*", (route) => {
    const req = route.request();
    const writes = req.method() !== "GET" && req.method() !== "HEAD";
    if (writes && ORDER_WORDS.test(req.url())) {
      console.log("DRY RUN blocked:", req.method(), req.url());
      return route.abort();
    }
    return route.continue();
  });
}

const page = await context.newPage();

Puppeteer does the same with page.setRequestInterception(true) and request.abort().

Two lines in there are easy to miss. Payment steps often open in a popup or a new tab, and a filter set on one page doesn't follow them; a filter on the whole browser context does. And a store's service worker can send requests your filter never sees, so Playwright's docs say to block service workers when you intercept requests.

Three details matter more than the code:

  1. Dry run is the default. The variable has to be set on purpose to go live. A forgotten flag should mean "safe", not "spend".
  2. Block too much, then loosen. The pattern above also blocks some harmless requests, like adding to cart on stores whose cart path says order. That's fine. Watch the log, and allow specific paths one by one.
  3. Log every block. The log tells you where the store's order request actually goes. That's the most useful thing a dry run teaches you.
The script talks to the store through a filter in the browser. Reading requests pass. Requests that would place an order are stopped and logged. your script request filter in the browser the store GET pages POST …/checkout/complete → stopped, logged

3. Pay with something that can't pay

Add a second, independent fence. If a store offers cash on delivery, choose it in tests: no card number exists to charge. If you must test a card form, use a card that will decline, and never one tied to real money.

Two fences that fail in different ways are far safer than one clever fence.

4. Check the cart before checkout, every time

The mistake I didn't expect wasn't the final click. It was the cart.

Stores remember carts. Run a test twice in the same session and the second run starts with the first run's items still there. Some stores also add one item twice when your script's click registers as two.

So before the script enters checkout, it reads the cart and compares it to what it meant to buy: same items, same sizes, same count. If anything differs, it stops. It doesn't try to fix the cart; it stops and says why.

5. Untick what you didn't choose

Checkout forms come with boxes already ticked: newsletters, marketing texts, "save my details", sometimes a paid add-on like gift wrap or insurance. A person would notice them. A script won't, unless you tell it to.

Before the last step, have the script find every checked checkbox on the page and log it. Untick everything that isn't required. Fail the run if a required box is one you don't recognise.

6. Record once, replay many times

Every run against a live store costs something: rate limits, bot checks, and the chance of a mistake. Once a dry run passes, save the pages and responses it saw. Playwright can record traffic to a HAR file and replay it.

Then test your edge cases offline: an item that sold out between cart and checkout, a missing size, a cart that came back with an extra item. You can stage those in a saved copy. On the live store you'd have to wait for them to happen.

What didn't work

  • Stopping on a page title. "Stop when the page says Review your order" broke on the first store that had no review page.
  • Running from a server. Some large stores refuse traffic from data-centre connections outright. No amount of waiting gets past that. If a store blocks you, accept it; working around a store's bot protection isn't testing anymore.
  • One fence. Every single safeguard I tried had a store where it didn't apply. Only stacking them held.

The checklist

  1. Dry run is the default; going live needs a deliberate switch.
  2. Order-placing requests are blocked in the browser, popups and new tabs included, with service workers off, and every block is logged.
  3. Tests pay with a method that can't charge anyone.
  4. The cart is checked against the intended items before checkout.
  5. Pre-ticked boxes are found, logged and unticked.
  6. Edge cases are replayed offline, not waited for live.

None of this is specific to shopping. Any script that sends an email, books a slot or moves money needs the same thing: a dry run the code can't talk its way out of.

taktekbot