Testing
The testing conventions - @effect/vitest, in-memory PGlite, focused tests, and the strongest-invariant rule.
The project’s testing standards live in .patterns/testing.md and every test follows them. This page explains the conventions and the concrete patterns used across the repo.
The two hard rules
- Use
expect/itfrom@effect/vitest, never from Vitest. Effect programs are asserted through Effect-aware assertions. - Every assertion must be able to fail, and assert the strongest invariant. No vacuous tests: un-awaited
.rejects/.resolves, expectations inside callbacks that may never fire, loops over possibly-empty collections, and conditional assertions are all banned. Assert exact values - a specific error class/code/message (never baretoThrow()),toBeovertoContain, real bytes not lengths.
Test placement
Tests live in __tests__ directories scoped to the package, or alongside the code:
- Platform tests:
platform/<package>/__tests__/. - Application tests:
packages/applications/<app>/__tests__/. - The retail service test lives at
packages/applications/retail/src/features/products/service.test.ts.
The service test pattern
The retail service.test.ts shows the standard way to test an Effect service. The key pieces:
In-memory database. Database.Test boots PGlite, so tests run without a Postgres server:
const testBase = Layer.mergeAll(Database.Test, Logger.Test);
Real migrations. The test applies the centralized migration files to the in-memory database, so the tables exist exactly as in production:
const applyMigrations = (sql: SqlClient.SqlClient) =>
Effect.gen(function* () {
const files = (yield* Effect.promise(() => readdir(migrationsDirectory)))
.filter((name) => /^\d+_.+\.sql$/.test(name))
.sort();
for (const file of files) {
const content = yield* Effect.promise(() =>
readFile(resolve(migrationsDirectory, file), "utf-8")
);
for (const statement of splitSqlStatements(content)) {
yield* sql.unsafe(statement);
}
}
});
Fresh environment per test. Each test provides its own layer, so tests never share database state.
What to test
Focused tests on the strongest invariants, not smoke tests:
- Tenant isolation - a product from one organization is invisible and untouchable from another.
- Permission gates - a caller without the capability receives
Forbidden, never data. - Validation - malformed input produces the typed
InvalidProducterror, not a crash. - Lifecycle transitions - illegal transitions fail with the typed transition error.
The retail test pins exactly these behaviors: scoped queries, requirePermission gates, and Zod validation, against the real migrations.
What not to write
- Endless smoke tests and “regression tests” for deleted features - the repo calls these slop.
- Snapshots you have not read - a snapshot captured from buggy code certifies the bug.
-updatecombined with a name filter - it can silently rewrite the wrong tests.
Running tests
pnpm test # every package's suite
pnpm nx run @africaos/retail:test # one package
pnpm --filter @africaos/retail test
Tests use @effect/vitest (from the catalog) and run through vp test. See vite-plus.