Skip to content

Testing Strategy

This page is policy and pyramid. For commands and per-framework usage, see Testing.

Pyramid Per Service

Each service has its own pyramid because the tooling and risk profile differ.

sirloin (Go) — wide unit base, real-DB integration tier

LayerToolWhat countsWhere
Unittesting + testifyPure handler / service / repo logic with mocked deps*_test.go next to source
Integrationtestify/suite + testcontainers (Postgres) + MinIO test server + bufconn gRPC serverReal DB, real S3, in-memory gRPCapps/sirloin/internal/pkg/testingsuite/
Sandbox / externalSame suite, build-tagged sandbox / integrationHits Chargebee sandboxmake run-tests-all
Coverage gateverify-coverage target in apps/sirloin/MakefileThreshold currently 3% (Check test coverage meets threshold (default 3%))CI gate

The sirloin pyramid is genuinely integration-heavyBaseTestSuite / GRPCTestSuite give you a real Postgres per suite, so most “unit” tests in sirloin are technically integration tests. This is by design (real DB catches BUN serialization bugs). See Testing for fixtures.

brain (NestJS) — unit-heavy with NestJS testing modules

LayerToolWhat countsWhere
UnitJest + @nestjs/testingService / processor logic with useValue mocked providers*.spec.ts next to source
IntegrationNone — no integration suite found in apps/brain/src/**; brain uses Jest unit specs only
Coverage gateNone enforced — apps/brain/package.json:24 runs jest --coverage but no coverageThreshold is set

brisket (Next.js) — component + hook unit tests

LayerToolWhat countsWhere
UnitVitest + @testing-library/react (not Jest — common gotcha)Component / hook / utility testscolocated with source
E2E / APIPlaywrightCross-service flows against a deployed stacktests/
Coverage gateNone enforced — apps/brisket/vitest.config.ts does not set coverage.thresholds

fennec (React) — typecheck + lint as the floor

LayerToolWhat countsWhere
Typetsc --noEmitCompile-clean TSpnpm tsc
LintESLint with --max-warnings 0Zero warnings toleratedpnpm lint
UnitVitest + @testing-library/react + @testing-library/jest-domComponent / hook testscolocated
Coverage gateNone enforced — no Vitest config in apps/fennec/ and no coverageThreshold in apps/fennec/package.json

flank (TS workflow engine)

LayerToolWhat countsWhere
UnitVitestWorkflow + adapter logiccolocated
Schema validationpnpm validate:seedsWorkflow seed JSON conformsapps/flank
Coverage gateNone enforced — apps/flank/vitest.config.ts does not set coverage.thresholds

round (Go ML) and strip (Go SSR)

LayerToolNotes
UnitGo testingmake run-tests with -race
IntegrationNone enforced — both services run only go test -race -count=1 ./... (apps/round/Makefile:5, apps/strip/Makefile:28); strip exposes test-coverage targets but no integration tier

Cross-service E2E

  • Playwright under tests/ (config tests/playwright.config.ts).
  • Run with cd tests && npm run test:api.
  • The Playwright config (tests/playwright.config.ts) defaults ENV=dev, pointed at https://fennec-dev-api.sexty.dev/ (tests/testConfig.ts). No GitHub workflow under .github/workflows/ invokes the tests/ Playwright suite — it is run manually (cd tests && npm run test:api).

Coverage Targets

ServiceTargetSource
sirloin3% (verify-coverage)apps/sirloin/Makefile — note this is a low floor, real coverage is much higher
brainNone enforced — jest --coverage runs but no coverageThreshold is configured in apps/brain/package.json
brisketNone enforced — apps/brisket/vitest.config.ts has no coverage.thresholds
fennecNone enforced — no Vitest config nor coverageThreshold in apps/fennec/package.json
flankNone enforced — apps/flank/vitest.config.ts has no coverage.thresholds
roundNone enforced — apps/round/Makefile only runs go test -race -count=1 ./...
stripNone enforced — apps/strip/Makefile:31-44 exposes test-coverage/test-coverage-summary targets but does not gate on a threshold

The sirloin 3% threshold exists to prevent regressions, not as an aspirational target. Treat current coverage as the de facto floor when you touch a package.

Mocking Policy

Go (sirloin, round, strip) — mockery v3

  • Mocks generated from interfaces by mockery v3, configured in apps/sirloin/.mockery.yml.
  • Output naming: mock_*.go, same directory as the interface.
  • Generate via make generate-mocks.

When to mock vs use real:

DependencyMock?Why
PostgresNo — use TestDatabase (testcontainers)BUN serialization bugs only surface against real PG
S3 / object storageNo — use EnableMinIOTestServer()Catches HMAC + bucket-policy issues
gRPC peers (brain, round)Yes — generated mockWe don’t want test runs cross-spawning services
ChargebeeYes for unit tests; real sandbox for integration-tagged testsSandbox is rate-limited (-p 1 enforced in run-tests-all)
Clock / randomYesDeterminism

TS (brain, brisket, fennec, flank)

  • Brain: Test.createTestingModule + useValue for provider injection. Mocks live inline in beforeEach per test file.
  • Brisket / fennec: prefer Testing Library + vi.mock for module-level mocks. Avoid mocking React components — use real DOM.

Test Data

ServiceMechanismSource
sirloinFixtureBuilder (functional options) — CreateUser, CreateCredit, CreateCharacter, CreateMedia with WithImageCredits, WithCharacterStatus, etc.apps/sirloin/internal/pkg/testingsuite/fixtures.go
brainNo centralised factories found in apps/brain/src/**/*.spec.ts; tests construct objects inline (e.g. media-processing.service.spec.ts).
brisket / fennec / flankInline data per test — no shared factory module under apps/{brisket,fennec,flank}/src/.

Seed scripts (production / dev environments rather than tests) live per service. The only first-party seed runners found are apps/flank/server/seed.ts and apps/flank/server/seed-sync.ts (workflow/adapter seeds). sirloin and brain have no committed dev-seed scripts; local data comes from make dev-up-d plus manual flows.

CI Test Execution

Workflow files in .github/workflows/:

WorkflowServiceTriggers testsNotes
sirloin.ymlsirloinmake ci-all mirror — verify-mod-tidy, verify-migrations, modernize, lint, govulncheck, build, run-tests, verify-coverageUnit only; run-tests-all (Chargebee sandbox) is gated
brain.ymlbrainpnpm installprisma generatepnpm lint / pnpm tsc / pnpm test (three parallel jobs in .github/workflows/brain.yml)
brisket.ymlbrisketpnpm installpnpm lint / pnpm typecheck / pnpm test (three parallel jobs in .github/workflows/brisket.yml)
fennec.ymlfenneclint + tsc with max-warnings 0
flank.ymlflanktypecheck + lint + test + validate:seeds
neon-branching.ymlinfraProvisions branched Postgres per PRSee Data Model
docs-quality.ymldocsmarkdownlint-cli2 over docs/**/*.md; lychee link-check (offline on PR, network on cron); pnpm build of Astro Starlight; ADR template check; proto sync check; docs-required check (.github/workflows/docs-quality.yml)
ai-hygiene.yml, pr-description-check.yml, up-to-date-check.ymlmetanon-test gates

There is no central monorepo test workflow — each service runs its own pipeline.

Flaky Test Policy

TODO(@law): no documented policy. Suggested: any test that fails non-deterministically must be either fixed within one business day or quarantined with a Linear issue referenced in the skip annotation. Update this section once the policy is ratified.

When to Write a Test

A test is required for any change that touches:

  • Billing or payments — see ADRs saga-pattern-for-distributed-payments, distributed-locks-for-payments, deferred-subscription-activation. These flows have non-trivial state machines and partial-failure paths.
  • Auth, RBAC, or session — anything involving Clerk, JWT verification, or StripAdminRole.
  • Generation pipeline — credit debit, brain generation insert, S3 upload, media row creation. The flow crosses three services.
  • Schema migrations — sirloin SQL or Prisma. Add a regression test that exercises the new column / constraint.
  • Public REST or gRPC surface — proto changes need handler tests.

A test is encouraged for everything else. A test is not required for: docs-only changes, dev-only scripts, generated code, or formatting-only PRs.

Cross-References

Open follow-ups

  1. Flaky test policy ratification (TODO(@law)).