Which tests to write first, and which can wait
In a young product, tests belong where a bug costs money, access or data. Everything else can wait without guilt.
An app built with an assistant usually has either no tests at all or a lot of tests checking that a button renders. Neither helps you sleep. Tests are insurance, and insurance is worth buying for things that are expensive to lose.
A simple filter: where would a bug cost you the most, and where would you be the last to find out? That’s where the first tests go.
Money
Anything that calculates, charges or credits. Bugs here don’t look like bugs. They look like a slightly different number, and someone notices a month later.
What to cover:
- price calculation: discounts, promo codes, taxes, rounding, currencies;
- payment webhook handling: a repeated event doesn’t credit twice, a refund revokes access;
- balances, credits and plan limits: you can’t go negative, and you can’t spend the same thing twice with concurrent requests.
That last one matters most. You can’t reproduce a race between two simultaneous requests by hand, but a test that fires two parallel calls can.
Access
Tests that prove user A can’t see or change user B’s data. Write them against the API, not the UI, because the UI already hides those buttons.
The minimum per resource: someone else’s record can’t be read, updated or deleted; a regular user can’t call an admin endpoint; a role sent in the request body is ignored. These tests are boring, easy to template, and they catch the most expensive class of bugs.
Data integrity
Some of the protection here isn’t tests at all but database constraints: not null, unique, foreign keys, check. They fire every time, not just when someone runs the suite. Start with those.
Add tests where constraints aren’t enough:
- migrations: they apply to a copy of the real schema and don’t lose data;
- cascading deletes: deleting an account removes what it should and nothing belonging to anyone else;
- background jobs: running one twice doesn’t create duplicates.
One or two end-to-end flows
An end-to-end test that goes through signup, the core action and payment in a real browser is slow and temperamental. But one or two of them on the main user path are worth the trouble: they catch breakage no single module can see. More than that can wait.
What you can skip for now
- UI snapshots. In a young product whose interface changes weekly, snapshot tests become a ritual of updating snapshots without reading them.
- Trivial code. Getters, thin wrappers, date formatting you can check by eye.
- Code you’re about to throw away. If a module is being rewritten next month, tests of its internals die with it. Keep tests of its outside behavior.
- Coverage percentage as a goal. The number grows fastest through code that didn’t need testing.
- Tests where everything is mocked. If the database, the payment provider and the clock are all fakes, you’re testing the fakes. For money and access, a real database in a container is better.
Keeping tests from rotting
Tests run in CI on every push, and a red test blocks the deploy. Otherwise, a month later half of them are red and everyone has stopped noticing.
A flaky test gets fixed right away or deleted. A test that fails “sometimes” teaches the team to ignore failures, which is worse than having no test.
And one rule that pays off faster than the rest: every bug found in money, access or data first gets a test that reproduces it, and only then a fix.
If you want to know where your product needs tests first, SUBXCAT can map it out and write the first set.