WooCommerce Checkout Testing: Building A Probe Safe To Run Daily.
A working synthetic probe that walks a real product to the payment step and stops. Seven rules, and the bug I hit building it.
Short answer: useful woocommerce checkout testing means holding a session, adding a real product, loading the checkout, asserting a payment method is present, and stopping before submit. The hard part is not the walking. It is doing it repeatedly against a live store without leaving anything behind.
I built one, ran it against a working store and a broken one, and got it wrong once on the way.
What it looks like running

returned 200 throughout the broken run.*
The isolation matters. A probe that fails entirely tells you something is wrong. A probe that passes four steps and fails two tells you where, which is the difference between a diagnosis and an alert.
The rules

the 500 it was built to detect.*
| # | Rule | Why |
|---|---|---|
| 1 | hold a cookie jar across every request | without a session, checkout only returns a redirect |
| 2 | assert on content, not just status | a checkout can return 200 with no payment method rendered |
| 3 | never submit the order | stop at the last screen, nothing to clean up |
| 4 | never raise on an HTTP error | a crash on a 500 gives a stack trace instead of a diagnosis |
| 5 | discard the session afterwards | otherwise you fill the client’s reports with abandoned carts |
| 6 | identify yourself in the user agent | explicable in logs, excludable from analytics |
| 7 | exit non-zero on failure | so cron, CI or a scheduler can act without parsing output |
That bug is worth dwelling on because it is easy to repeat. My probe called urlopen and let exceptions propagate. Against a healthy store it worked perfectly. Against the broken store, the exact scenario it existed for, it produced a Python traceback instead of a report.
A probe must be most reliable at the moment things are worst. Every request wrapped, every failure turned into a result rather than an exception.
Rule three is the one with commercial consequences. It is genuinely easy to write something that places an order every hour, and every one of those lands in the client’s order list, their revenue reports, their tax records, and possibly a warehouse picking queue with a real person attached to it.
What to assert at each WooCommerce checkout testing step

both return 200 and both render fine.*
| Step | Lazy assertion | What it misses |
|---|---|---|
| product page | status == 200 | out of stock, or purchase button removed |
| add to cart | status == 200 | the item never actually entered the cart |
| cart page | status == 200 | an empty cart also returns 200 |
| checkout | status == 200 | payment methods missing, or a fatal inside a normal template |
| any step | no timing check | a 30-second checkout is functionally broken and returns 200 |
For checkout specifically, assert that a payment method actually appears in the HTML. A gateway plugin that stopped loading leaves a checkout page that renders, returns 200, and cannot take money. Status codes will never see it.
Add a timing assertion too. A checkout that takes thirty seconds is functionally broken, and it passes every status check ever written.

a failed check and dying with a traceback at the moment somebody needs an answer.*
Where to run it from
Outside the site, on a schedule you did not choose, from somewhere that is not your laptop.
If it only runs when you remember, it will be green on the day it matters. Cron, a CI schedule, or a monitoring service that can execute a script are all fine. What matters is that the failure exits non-zero and something is listening.
Run it more often than feels necessary on a store that trades all day, and less often on one that takes a handful of orders a week. The cost of a run is three HTTP requests; the cost of finding a broken checkout six hours late is measured in orders.
Exclude the probe’s traffic from analytics, or you will add a daily phantom session that reaches checkout and never converts, which will quietly damage the funnel numbers the client reports on. A distinctive user agent makes that filter trivial, which is why rule six exists.
Choosing the product to probe
This is a small decision that decides whether the probe stays useful.
Use a real, purchasable product, not a hidden test item. A test product with no stock rules, no shipping class and no tax band exercises none of the logic that actually breaks. The failures worth catching live in the interaction between a real product and real settings.
Pick something stable. A seasonal item that goes out of stock will fail the probe for a completely legitimate reason, and a probe that cries wolf gets muted. If the client’s catalogue turns over, ask them which product is permanent.
Do not use the most expensive item. If anything ever does go wrong and an order is created, you would rather it were the nine pound one.
And revisit the choice occasionally. A probe pointed at a product that was discontinued eight months ago has been testing a 404 and passing on it, which is its own quiet failure.
Building the exact probe and finding its own blind spot
I wrote the probe this post describes, roughly sixty lines of standard-library Python, a shared cookie jar, wrapped requests, content assertions, and ran it against a real WooCommerce install with a real product. Healthy run first:
product page status=200 436ms PASS
add to cart status=200 169ms PASS
cart page status=200 126ms PASS
checkout page status=200 130ms PASS payment_mentioned=False
That last field is the problem. The checkout page assertion this post recommends, confirm a payment method actually appears in the rendered HTML, returned False on a genuinely healthy store. This install runs WooCommerce’s newer block-based checkout, which renders payment options through client-side JavaScript calling the Store API rather than embedding them in the server-rendered page, the same architecture distinction found while testing conversion tracking on this same install. A plain HTTP probe, no JavaScript execution, never sees payment method markup either way, healthy or broken.
I confirmed that was a real blind spot rather than a quirk by deliberately disabling every payment gateway on the store, an unambiguous checkout failure, no customer could complete an order, and running the probe again:
product page status=200 303ms PASS
add to cart status=200 134ms PASS
cart page status=200 143ms PASS
checkout page status=200 129ms PASS payment_mentioned=False
Identical result. PASS, on a checkout with zero working payment methods. The exact assertion this post recommends as the thing status codes cannot catch failed to catch the thing it exists to catch, on this specific checkout architecture, because the signal it looks for was never present in the raw response to begin with, working or not.
The instinct is to redirect the check at the WooCommerce Store API’s own cart endpoint, /wp-json/wc/store/v1/cart, rather than scrape rendered HTML at all, and I tried exactly that on the same install. It did not simply work: the request redirected and the response that came back was the site’s own homepage HTML, not the structured cart JSON the endpoint is documented to return, the same key-stripping redirect behaviour found earlier while testing this Store API path for a different reason. That is worth reporting honestly rather than presenting an untested fix as a solved problem: the plain-HTML assertion clearly does not work against a block-based checkout, and the API-based alternative, on this specific install, did not work cleanly either on a first attempt. Getting a reliable Store API read working, correct session handling, correct nonce, following the exact redirect this install produces, is real additional work this post cannot claim to have finished, and any team building this probe against a block-checkout store needs to budget time for exactly that discovery rather than assume the Store API route is a drop-in replacement for the HTML assertion.
What this does not replace
A probe tests one product through one path. It will not catch a specific variation being unpurchasable, a shipping method failing for one country, or a discount code that stopped applying.
For those, the signal is the failed-order rate against the store’s own baseline, which measures outcomes rather than a single path.
And it says nothing about whether the cart is being served from cache to the wrong customer, which is a separate check entirely and arguably the more expensive failure.
It is worth pairing with a check on the page’s own speed, because a probe measuring only correctness will happily pass a checkout that has become unusably slow. The timing numbers the probe already collects are enough, compared against what the origin normally does rather than against an arbitrary threshold.
Protuno’s eCommerce agent, Till, runs a read-only WooCommerce Health Review inside your own AI app. See the playbooks page for what it covers.
The probe above is about sixty lines, uses nothing outside the Python standard library, and it is the version I would write again. Point it at a staging store first, confirm it passes, break the checkout deliberately, and confirm it fails.
That last step is the one to insist on. A test you have never seen fail is not a test, it is an assumption with a green tick next to it, and the whole category of problem this blog keeps returning to is assumptions that were never checked.
The lesson this specific build reinforces
Building the probe and deliberately breaking the store twice, once by fataling the whole site and once by disabling every payment gateway while leaving everything else untouched, produced exactly the outcome that step is meant to catch: the first break was caught immediately and obviously, every step failing with a 500. The second, the more realistic and more dangerous failure, passed cleanly, a PASS on a checkout that could not actually take an order.
That gap between the two results is the entire argument of this post’s insistence on the break-it-deliberately step, demonstrated rather than merely stated. A probe that has only ever been run against a healthy store looks identical whether its assertions are correct or completely hollow, right up until the day it needs to catch something real. The only way either of us, the person writing this probe or the person deploying someone else’s, finds out which kind of probe they actually have is by breaking the specific thing it is meant to catch and watching whether the result changes. On block-based checkout specifically, that step is not optional. It is the only way this post’s own recommended assertion would have been caught as insufficient before a real checkout outage went undetected by a monitor everyone trusted.
Comments