If an AI writes your code, your tests are the only thing standing between "it compiles" and "it's right". So in ParkEase, tests weren't a phase at the end. They were the contract the generated code had to meet.
By the end: 3,246 unit tests and 1,080 integration tests, plus browser end-to-end suites. Here's how they came to exist, and how they were kept honest.
1. Tests first, every time
The build phase of the workflow is test-driven development: write the failing test, watch it fail, write the code, watch it pass. That order matters more with an AI than with a person. An agent writing tests after the code tends to write tests that describe what the code does, bugs included. A test written first describes what the code should do.
2. Every task gets a test plan before it gets code
ParkEase was built as 22 tasks, and each one has an entry in a test registry: what will be tested, at which level, and why. That file reached 2,357 lines. It's what made "is this feature tested?" a lookup instead of an opinion, and it's updated as part of shipping, not afterwards.
3. Real infrastructure, not mocks
Integration tests run against a real PostgreSQL 18 with PostGIS in a throwaway container. No mocked database.
A mock is a second implementation of your database written by the same author, AI or human, with the same assumptions. ParkEase's most important rules live in the database itself: overlapping bookings are refused by an exclusion constraint, the ledger must balance. You can't test those against a mock that doesn't have them.
4. Four suites that can never be red
Four integration suites are mandatory on every change:
- booking concurrency: the last slot sells exactly once. One test drops the database constraint and checks that the double-sell comes back, proving the constraint is what's doing the work.
- ledger balance: debits equal credits for every transaction.
- idempotency: a retried request never charges twice.
- search performance: p95 under 200 ms over 10,000 spaces, and the test fails if the query plan stops using the spatial index.
Plus stress tests: 100 drivers racing for 10 slots must produce exactly 10 bookings and 90 clean conflicts, no server errors, p95 under 2 seconds.
5. Test at the level the bug lives
Two lessons became rules after they let defects through:
- API tests go through HTTP. Tests that call a command directly skip the guards, interceptors, status codes and error envelopes. Now API tests send real requests through the real stack.
- New screens need new tests. The mobile app ended with 1,466 component tests, plus a table-driven test that checks every data screen has a loading, an error and an empty state.
The web and admin apps get Playwright end-to-end runs at phone and desktop sizes, with an accessibility scan on every page.
6. Green locally, before the PR
The last gate before any pull request: lint, typecheck, every unit suite, the four mandatory integration suites, a dependency audit and a secret scan, run locally, with the output pasted into the PR. "CI will catch it" isn't a reason to open early.
Where the tests weren't enough
Two honest gaps.
Fixtures can lie. The first deploy found three bugs that every test agreed with, because the test fixtures and the code shared the same wrong assumption about the API. The fix was making tests use the real response shapes, imported from the shared contracts instead of hand-written.
No phone, no test. The end-to-end mobile flows were written but need a real Android device to run. That gap is logged, not hidden.
Next: harvesting. How every mistake, review finding and deferred idea became a written row somewhere, and the hook that won't let a task finish until it does.
ParkEase is a peer-to-peer parking marketplace for India, built solo with Claude Code.
Code: https://github.com/Deonkar/parkease
