One flag, for the whole project, for faking side effects locally. Not one flag per feature: NEWSLETTER_DRY_RUN, SKIP_EMAILS, STRIPE_FAKE and friends multiply, get forgotten, and nobody remembers which ones a fresh checkout needs.
The convention
-
One variable:
TEST_MODE=1, set in.env.local(which must be gitignored — check). Nothing else. -
One helper reads it, and every gated feature imports the helper rather than reading the env directly. Put it where the project keeps shared server code, e.g.
src/lib/test-mode.ts:// Development only: production can never fake a side effect, whatever its env says. export const testMode = process.env.NODE_ENV === 'development' && process.env.TEST_MODE === '1'; -
Dev only. The
NODE_ENV === 'development'check is the point — a strayTEST_MODEin a production environment must do nothing. A consequence worth knowing: a local production build (next build && next start,vite preview) won't honour it either. -
Server side. Side effects happen on the server, so the helper is server-only. Don't expose it to the client bundle (no
NEXT_PUBLIC_/VITE_prefix).
Gating a feature
At the point where the real call would happen, act as if it worked and log what was skipped:
import { testMode } from '@/lib/test-mode';
if (testMode) {
console.info('Test mode: skipped sending the welcome email');
return { ok: true };
}
await sendWelcomeEmail(user);
- Return the success shape the real call would, so the rest of the flow (redirects, success messages) runs as normal.
- Log with the
Test mode:prefix so it's obvious in the dev console what didn't happen. - Still run validation and everything before the side effect — test mode skips the send, not the logic.
Document it
Keep a "Testing locally" section in the README that says how to turn test mode on and lists every feature it gates, one line each. Add the line in the same PR that gates the feature.
Consolidating existing flags
If the project already has per-feature dry-run flags, propose replacing them with testMode: list each flag, where it's read, and its .env entries. Ask before removing them, since someone's local setup may depend on them; then migrate and update the README in one PR.
Not for
- Automated tests. Unit tests should mock the sender directly, and e2e tests running against a production build won't see test mode — intercept the network call there instead (Playwright's
page.route). Seetest-strategy. - Staging environments. Use the provider's sandbox or test keys for those, not this flag.