Two build gates that make RN quality non-negotiable
React Native apps don't usually fail loudly. They rot quietly. A refactor leaves a screen that
return null and ships a blank page. A "temporary" stub passes lint, type-check, and
unit tests because nothing renders it. A business rule everyone agreed on — "the wallet
changes when you spend" — is never actually asserted, so it silently breaks three releases later.
Tests don't catch these because tests check logic, and these are integration
truths. So I made them impossible to skip, with two build gates that fail npm run check
and CI.
Gate 1 — a screens gate: a screen must actually be a screen
A stub screen (returns null, or renders nothing) is invisible to logic tests. This gate
walks src/ui/screens and asserts every file is a real, default-exported component that
renders markup:
// src/__tests__/screens.gate.test.tsx
import fs from 'fs';
import path from 'path';
const SCREENS_DIR = path.join(__dirname, '..', 'ui', 'screens');
const MIN_LINES = 20;
const files = fs.existsSync(SCREENS_DIR)
? fs.readdirSync(SCREENS_DIR).filter((f) => f.endsWith('Screen.tsx'))
: [];
describe('every screen is a real implementation (not a stub)', () => {
it('has screen files', () => expect(files.length).toBeGreaterThan(0));
files.forEach((file) => {
it(`${file} is implemented`, () => {
const src = fs.readFileSync(path.join(SCREENS_DIR, file), 'utf8');
expect(src.split('\n').length).toBeGreaterThan(MIN_LINES);
expect(src).not.toMatch(/return\s+null\s*;/);
expect(src).toMatch(/export\s+default/);
const mod = require(path.join(SCREENS_DIR, file));
expect(typeof mod.default).toBe('function');
});
});
});
It's ~25 lines, and it has caught a black-screen launch more than once.
Gate 2 — an acceptance-contract gate: no unproven invariants
Instead of hoping people remember to test the important things, you declare them as a
machine-checkable contract (contract/acceptance_spec.json):
{
"invariants": [
{ "id": "INV-PERSIST-1", "assertion": "settings round-trip through storage unchanged" },
{ "id": "INV-STATE-1", "assertion": "changing a setting leaves unrelated fields untouched" }
],
"journeys": [
{ "id": "J-SETTINGS-1", "name": "settings persist, reload, and reset" }
]
}
Then a gate test fails the build unless every id is referenced by at least one test — and it rejects fake coverage: tests that only assert on literals they defined themselves don't count, and contract-tagged test files must import real code.
The point
Both gates are cheap and boring — which is exactly why they work. They turn quality from a habit
into a property of the build. npm run check (strict TS + ESLint + Jest)
stays green because it has to.
Typed navigation, deep links, theme tokens, MMKV persistence, native modules mocked for tests.
Get the repoThe gates above, plus CI and local iOS/Android release scripts, are packaged in the Expo Production Starter.
Get new posts
One email when a new post ships. No spam.