Why it took us three years to start writing automated tests (and what changed our minds)

For the agency’s first three years we didn’t write a single test. Zero. We’d build the site, click around it for a while before handing it over, and that was that. It worked, until it didn’t.
Writing automated tests is worth it above all for logic that touches money or availability — pricing, discounts, bookings — not the whole project, because that’s where a silent failure gets expensive, the same kind of thing as the dependency patch that took down a checkout, and an afternoon’s worth of tests keeps it from happening again. The project that changed our minds was a booking site for an outdoor tourism business (kayaks, guided routes, that kind of thing). They had a booking form with fairly gnarly availability logic: blocked dates, a max capacity per activity, group discounts. Every time we touched anything in the calendar, we’d break something else without noticing. And we weren’t the ones who caught it: the client did, when someone managed to book the same slot twice.
It happened three times in a month. By the third time the client called us pretty annoyed, fair enough.
That’s when we put Vitest into that project, just for the availability logic, nothing else. No chasing 100% coverage or any grand quality process: we took the four functions that calculated free slots and wrote tests for the exact cases we already knew broke. It took a day and a half. That logic hasn’t broken once since, in a year and a half.
What took us a while to get was that you don’t need to test an entire project for it to be worth it. We still don’t have a single test on 90% of the code we write: static pages, simple contact forms, things that if they break are obvious within five minutes just by looking. Where we do add tests now is calculations: discounted pricing, availability, anything that depends on dates or on combinations of conditions a human doesn’t check well in their head.
I use a simple rule to decide. If a bug in that function costs the client money directly (a double booking, a wrong price) or if the failure is silent, no error thrown, just the wrong result quietly sitting there, I test it. If the failure is visual and obvious, I don’t bother.
It’s not free, either. Some projects don’t have budget for more than 40 hours of development, and writing tests is time you’re not billing as visible functionality. A new client has a hard time understanding why something invisible took two extra days. With returning clients, the ones we already have trust with, I just say it straight: this is what keeps me from getting a call from you in August because something broke. With a one-off client, honestly, sometimes I don’t even bring it up and just eat the hours internally.
What I still haven’t figured out is when Playwright end-to-end tests are worth it over just testing the logic. We’ve used them on two projects, and on one they got so brittle with every CSS change that we ended up deleting them four months in. I don’t know if that was down to how we wrote them or if they simply don’t fit projects whose design keeps shifting every few weeks.