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 type | Location |
|---|---|---|
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/controllers | backend/test/v1/<module>/*.e2e-spec.ts |
backend/src/integration/** (integration plugins — crawler, ftp, gmail, hello-world — and the polling/task-execution engine) | Unit tests | Co-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.ts | Unit 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 inbackend/test/v1/user-integration/*.e2e-spec.ts— don't add auser-integration.service.spec.ts. - Building a new integration plugin? Follow
ai/skills/integrations/SKILL.md— unit test it atbackend/src/integration/<domain>/test/test.spec.tswith mocked network calls, per the existingftpexample. - Changing
PollingScheduler/PollingProcessorbehavior? Extendpolling.service.spec.ts/polling.processor.spec.ts— this is library-like execution-engine code, not a v1 CRUD module.