one test to rule them all
What I'm aiming for:
make test
One test harness everywhere. If wiring it to a new environment needs special glue, the issue is almost always in your system boundaries, not the tests. And it doesn't need to be make. It could be cargo, go, bazel.
One entry point. Let config handle the differences. Sometimes it's simple:
ENV=local make test
ENV=ci make test
ENV=staging make test
ENV=prod make test
Sometimes it needs more variables:
AWS_REGION=us-west-2 \
DB_HOST=test-db.internal \
REDIS_HOST=localhost:6379 \
RATE_LIMIT=false \
LOG_LEVEL=debug \
TENANT_ID=synthetic-test \
make test
The variables don't matter. Neither does how we inject them. What matters is the harness stays the same and it's one entry point.
Many teams test twice: mocks, then real. When results disagree, adding more mocks or more envs rarely helps. Usually the test (or the abstraction around it) was built against a contract the real system never offered. Fakes and mocks can lie when they assert promises nobody made. This requires explicit contract testing: running the same suite against both the fake and the real service to ensure they never drift. If a new test needs dark magic, I've probably built more fiction than system. Tests might pass, but they're checking the wrong thing.
Tests shouldn't know where they run. The harness owns wiring. Tests own correctness. This only works when tests are hermetic and deterministic. Time, randomness, IO, credentials, dependencies — control what you can. Otherwise you can't trust your test.
Speed matters, but not at the cost of correctness. The sequence that works for me: fix the slow code, then the slow test, then filter the slow tests. Most of the pain disappears with simple tags or build selectors: test sizes, -short in Go, #[ignore] in Rust.
When it doesn't? Feels like architecture leaking. Local tests that need real AWS to start, or staging tests that need ritual hacks to run. These aren't useful tests. They probably overlap with other tests that are more correct and valuable.
That's where modular boundaries and IO layers help. Push the messy details to the shell. Keep the core boring.
- One test harness.
- One entry point.
- Everything else is config and good code.