Let's talk
operations

The same end-to-end suite runs before the push and after the deploy, pointed at a different host

For a while the only place the end-to-end tests could run was production. There was no local environment with realistic data, so a change went out and then the browser-driving suite confirmed it against the live system, on the customer’s real records.

That works exactly until the day something is wrong. Then you have found a defect by demonstrating it on live data, and the customer’s quotations are the test fixtures.

The change that fixed it was smaller than it sounds. The suite already drove a real browser against a real deployment. It only needed to stop assuming which deployment.

One suite, two base addresses

The suite reads the interface address and the API address from the environment. In production they are the same address, because the interface and the API are served from one origin behind the same proxy. Locally they are not — the development server for the interface does not proxy the API — so the two are supplied separately, with the API address defaulting to the interface address when it is not given.

That is the entire mechanism. Two environment variables and a default.

What it buys is that there is exactly one suite. Not a pre-push suite and a post-deploy smoke test, which is the usual arrangement and which reliably produces two things that check different subsets of behaviour and drift apart over a year until neither is trusted. The tests that gate the push are the tests that verify the deploy, so anything they check is checked in both places by construction.

The local stack, built from real data

The local half is a restored snapshot of the production database into a separate local database, brought up to the current schema, with the API pointed at it and the interface pointed at the API.

Using a restored snapshot rather than seeded fixtures is the part that has actually caught things.

A change to how dimensions are converted between measurement systems had a write path I had missed — one editing surface that stored a value without converting it. Against fixtures, that bug produces a number, and a number is a number. Against real records, it produces a work table that is a twenty-fifth of its correct price, sitting next to forty other lines that look right. Magnitude errors are visible only when the surrounding magnitudes are real.

The obligations that come with a production snapshot are the usual ones and they are not optional: it lives locally, it is not shared, and anything done against it is done knowing it contains a real business’s commercial data. A snapshot is a copy of the thing you are protecting.

Tolerating one absence without tolerating failure

The document renderer is a native dependency that is installed on the server and not on every development machine. A local API without it cannot produce the printed quotation, and returns a specific rejection saying the capability is unavailable.

The suite tolerates exactly that: that endpoint, that specific client-error response, meaning the renderer is absent. It does not tolerate a server error from the same endpoint, which would mean the export path itself is broken. And because tolerating anything leaves a hole, the document template is separately exercised by rendering it directly, so the part that can be checked without the native dependency still is.

This is a pattern worth stating generally, because the lazy version of it is everywhere. Tolerate a named absence by its exact signal, never by widening what counts as success. A test that skips on any error from an endpoint is a test that will one day skip on the outage you needed it to catch, and it will report green while doing it.

Cleaning up after yourself on a live system

Because the same suite runs against production, it creates real records in a real system. Every quotation it creates is labelled with a recognisable prefix and deleted on teardown.

The labelling matters more than the deletion. Teardown fails sometimes — a timeout, an interrupted run, an assertion that stopped the suite early. When it does, the leftovers have to be identifiable by someone who was not there, without guessing from timestamps. A test artefact that is indistinguishable from real data is a test artefact that ends up in a revenue report.

Verifying a deploy in three assertions

The deployment routine that goes with this is deliberately boring, and it is the same every time: back up the database, deploy the code, apply the schema migrations, then verify.

Verification is three things. The health endpoint responds. The applied schema version matches the expected one. And a specific piece of data that the change was supposed to affect is checked directly — the setting that should now exist, the rows that should now have descriptive names, the value in a particular rule that was supposed to be corrected.

That third one is what makes it a verification rather than a formality. A service that starts and a migration that ran are necessary conditions and prove nothing about intent. Naming, in advance, one observable fact that will be true only if the change did what it was meant to — and checking it — is the difference between a deploy that succeeded and a deploy that completed.

Then the suite runs against the deployed system, and because it is the same suite that gated the push, a pass means the same thing in both places.

The limit

This does not cover everything. The pre-push run happens against a snapshot taken at some point in the past, so a change that interacts with data created since will behave differently, and there is no continuous integration running any of it automatically — it is a discipline, executed by a person, and disciplines executed by people are skipped under time pressure.

The honest description is that it removes the class of failure where a change is first exercised on live data. It does not remove the class where nobody ran the tests.

The rule

Write one end-to-end suite and parameterise it by the address it points at, so the same assertions gate the push and verify the deploy. Run it locally against restored real data rather than fixtures, because magnitude errors are invisible in synthetic numbers.

And when a local environment genuinely cannot do something, tolerate that one signal precisely, never by broadening what your tests are willing to accept.

Working on something like this?

We build this kind of software, and we staff the teams that do.

Get in touch