---
title: Testing
description: 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

1. **Use `expect`/`it` from `@effect/vitest`, never from Vitest.** Effect programs are asserted through Effect-aware assertions.
2. **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 bare `toThrow()`), `toBe` over `toContain`, 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:

```ts
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:

```ts
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 `InvalidProduct` error, 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.
- `-update` combined with a name filter - it can silently rewrite the wrong tests.

### Running tests

```bash
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](/tooling/vite-plus).