M.Sayed

// notes / 2026-10-13 · 6 min

What 1,101 Tests Taught Me About Flutter Clean Architecture

Flutter / Testing / Architecture

A year ago Al Batal Elite had 397 tests and I called that thorough. It has 1,101 now, and the number that matters isn't the count. It's that I can delete a whole layer and watch exactly which contracts break instead of guessing.

Tests that pin contracts, not implementations

Most of my early tests asserted how a cubit works. They broke every time I refactored, so I started deleting them, the worst possible outcome for a suite. The tests that survived are the ones that pin contracts: what the code promises, not how it promises it.

  • Cubit tests assert the observable state after an action, never the call sequence inside.
  • SQL migration contract tests pin the checkout RPC's server-owned pricing, so a schema rewrite that changes a total fails loudly.
  • An adversarial RLS suite (44 checks) treats permissions as an attack surface, not a feature.
  • A race-condition suite (53 checks) places concurrent orders against the same stock and verifies the invariants hold.

The tests that found real bugs

Three suites exist because something needed guarding. The payment security tests assert no client-side verification and redact tokens from error messages. The contrast tests audit every color pair in light and dark themes against WCAG AA, so accessibility is a test, not a checklist someone ticked once. The asset rule tests reject any runtime image that isn't an SVG, the cheapest guard against a debug screenshot shipping as an app icon.

The rule that keeps a suite alive

In Shop Admin I enforce one contract on the suite itself: every file in lib/ must be referenced by a test, or listed in an audit allowlist with a written reason. New code without a test fails CI. It's draconian for the first week, and after that it's the reason the suite never rots, because a file nobody tests is a decision someone made twice: once when writing it, once when letting it ship untested.

A test suite is a design document that executes. When it goes red, the design changed, not the test.

What I would do differently: start the contract tests first, not last. The checkout RPC tests took a weekend to write and would have caught pricing drift while the code was still warm. The pattern is consistent: the tests that cost the most to write are the ones that never need rewriting.

← All notes