Skip to main content

Testing

This page defines what kind of test to write for a given piece of backend code. Getting this wrong is the fastest way to end up with either brittle mock-heavy tests that don't catch real bugs, or missing coverage on code that genuinely needs isolated unit tests.

The rule

Code lives under...Test typeLocation
backend/src/v1/** (app modules: user, user-session, user-integration, user-integration-content, search, integration, and friends)E2E only — no .spec.ts unit tests for services/controllersbackend/test/v1/<module>/*.e2e-spec.ts
backend/src/integration/** (integration plugins — crawler, ftp, gmail, hello-world — and the polling/task-execution engine)Unit testsCo-located: backend/src/integration/<domain>/test/test.spec.ts, or next to the file for polling (polling.service.spec.ts, polling.processor.spec.ts)
backend/src/**/entities/*.entity.tsUnit tests (existing pattern, keep it)Co-located: *.entity.spec.ts

Why the split: the v1 app modules are thin CRUD/orchestration layers over Postgres, Redis, and each other — their real behavior only shows up when exercised through the actual HTTP surface against a real database (auth guards, DTO validation, TypeORM relations, session cookies all interacting together). Mocking all of that to unit-test a service in isolation tests the mocks, not the system. Integration plugins and the polling engine are the opposite: self-contained logic (pagination, idempotency, task routing, manifest parsing) that's slow and flaky to exercise through the full HTTP stack and is much better tested in isolation with mocked network calls — see ai/skills/integrations/SKILL.md's Testing Structure section.

If you're adding coverage for a v1 service or controller, write an *.e2e-spec.ts under backend/test/v1/<module>/, not a new .spec.ts next to the source file.

Running tests

cd backend

# Unit tests (integration plugins, polling engine, entities)
pnpm run test # once
pnpm run test:watch # watch mode
pnpm run test:cov # with coverage

# E2E tests (v1 app modules, full HTTP stack)
pnpm run test:e2e

E2E tests use backend/test/util/app.module.ts's App helper, which boots the real AppModule (not a mock) with PollingModule's cron job stubbed out, an in-memory session store, and supertest driving real HTTP requests. Each spec file's beforeAll/afterEach clears the database via App.clearDatabase() (TRUNCATE ... RESTART IDENTITY CASCADE on every table) — tests are not expected to coexist with data you care about.

Test database

backend/test/setup.ts loads backend/.env.test if present (falls back to CI-provided environment variables otherwise). This points at a dedicated test database, separate from local dev data — check POSTGRES_DB/POSTGRES_URL in .env.test before running test:e2e if you're unsure which database it targets. Running the e2e suite truncates every table in that database on every run; never point .env.test at a database with data you need.

What this looks like in practice

  • Adding a field to UserIntegration? Add/extend an assertion in backend/test/v1/user-integration/*.e2e-spec.ts — don't add a user-integration.service.spec.ts.
  • Building a new integration plugin? Follow ai/skills/integrations/SKILL.md — unit test it at backend/src/integration/<domain>/test/test.spec.ts with mocked network calls, per the existing ftp example.
  • Changing PollingScheduler/PollingProcessor behavior? Extend polling.service.spec.ts/polling.processor.spec.ts — this is library-like execution-engine code, not a v1 CRUD module.