devops
A testing kit that isn't tied to a stack
A testing kit that isn't tied to a stack
EduQate's tests grew into five CI workflows: backend, frontend, browser E2E, hygiene and security, and a weekly scheduled run. I don't only work in Node, and I wanted the same safety net on a .NET or Rust project without rebuilding it by hand. So I pulled the layout out into a kit.
Three layers, only one is stack-specific
- A stack adapter — one small Makefile per stack (
node.mk,dotnet.mk,rust.mk,nextjs.mk,angular.mk). It maps each kind of test to the command that runs it:unitis Jest on Node,dotnet test --filter Category=Uniton .NET,cargo test --libon Rust. - A Makefile with fixed target names —
unit,api,db,races,cache,sec,smokeand so on. CI only ever callsmake <target>, so the workflows don't change when the stack does. A target with no command for that stack prints[skip]instead of failing. - Tests that don't care about the stack — Playwright, k6, smoke checks, CodeQL, Trivy, actionlint. They talk to a running system over HTTP, so they work the same against Node, .NET or Rust.
A mixed repo sets a different stack per workflow: .NET for the backend workflow, Angular for the frontend one.
Folder N runs in workflow N
| # | Workflow | What's in it |
|---|---|---|
| 1 | Backend and services | unit, API integration with a route-by-role guard matrix, real Postgres, race tests, real Redis, migration safety, one regression test per bug, security specs, fresh-boot smoke |
| 2 | Frontend apps | shared packages, lint, component tests, build, mobile |
| 3 | E2E browser | one journey per role, an access matrix, double submits, empty states, i18n and RTL, 2FA, axe accessibility checks, visual baselines |
| 4 | Hygiene and security | test lint, actionlint, reverse-proxy config check, CodeQL, Trivy |
| 5 | Scheduled | mutation testing, k6 load, backup dump and restore |
Numbering the folders after the workflows sounds trivial. It's what lets me find "where does the race test for X live" without thinking.
The rules that came with it
- Done means seen working. A change with a runtime surface is done when I've used it in the running app with no new console or network errors, not when it compiles.
- Real dependencies where the bug lives. Mock the payment provider and the email service. Don't mock your own database, cache or HTTP stack in integration tests — mocked tests agree with the code, not with reality.
- Every bug becomes a test, named after its ID in the problem register, and never deleted.
- Security is a test, not a review. Anonymous access, wrong role, one user reading another's record, rate limits and secret scanning all run in CI.
- Heavy runs go to CI. Coverage, mutation and load testing are slow and hungry. They don't belong on a laptop.
- A flaky test is a bug. Wait for the real signal instead of sleeping, give each test its own data, or quarantine it where everyone can see. Never retry until green.
Growing it in a new project
Smoke and one unit test first, so CI is green from day one. Then API tests for auth and the main flow, then the guard matrix and security specs. Database, race and migration checks come as soon as there's real data. Then one Playwright journey per role. Visual baselines wait until the UI settles. Mutation, load and backup-restore runs go in before launch.