Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
29 KiB
Testing Standards
This document outlines testing conventions, file organization, and naming patterns for the Alga PSA codebase.
Table of Contents
- Test Directory Structure
- Test Type Guidelines
- File Naming Patterns
- Test Organization Decision Tree
- Running Tests
- Test File Templates
- Key Principles
Test Directory Structure
server/src/test/
├── unit/ # Isolated tests with mocked dependencies
│ ├── components/ # React component tests
│ ├── app/ # App-level tests (mirroring app structure)
│ ├── project-actions/ # Domain-specific action tests
│ └── workflow/ # Workflow-related tests
├── integration/ # Multi-component tests with real database
├── infrastructure/ # Full system scenarios (billing, invoicing, etc.)
├── e2e/ # End-to-end API and workflow tests
│ ├── api/ # REST API endpoint tests
│ ├── email-settings/ # Email workflow tests
│ ├── factories/ # Test data factories
│ ├── fixtures/ # Test fixtures
│ └── utils/ # E2E test utilities
├── mocks/ # Shared mocks
└── utils/ # General test utilities
ee/temporal-workflows/src/__tests__/
├── e2e/ # Temporal workflow E2E tests
└── activities/__tests__/ # Temporal activity tests
Test Type Guidelines
1. Unit Tests (server/src/test/unit/)
When to use: Testing individual functions, classes, or components in isolation
Naming convention: <feature>.test.ts or <ComponentName>.test.tsx
Location options:
- Centralized (preferred):
server/src/test/unit/ - Colocated: Next to source file (e.g.,
lib/auth/sessionCookies.test.ts)- Only use for utilities that are deeply coupled to their implementation
Examples:
bucketUsageService.test.ts- Service function testsauth.test.ts- Authentication logic testsEmailProviderConfiguration.test.tsx- Component tests
Characteristics:
- Heavy use of mocks and stubs via Vitest's
vi.mock() - No real database connections
- Fast execution (< 1 second per test)
- Test single responsibility/function
- Use
@vitest-environment jsdomcomment for React component tests
Subdirectory organization:
components/- UI component testsapp/- Mirror app directory structure for page/route tests- Domain-specific folders (e.g.,
project-actions/,workflow/)
2. Integration Tests (server/src/test/integration/)
When to use: Testing multiple components working together with real dependencies
Naming convention: <feature>Integration.test.ts or <feature><Purpose>.test.ts
Examples:
bucketUsageIntegration.test.tsemailProviderIntegration.test.tsgoogleProviderDatabase.test.ts
Characteristics:
- Use real database connections via
createTenantKnex() - Test interactions between services, actions, and data layer
- Test data persistence and retrieval
- May use
TestContextfor setup - Moderate execution time (1-5 seconds per test)
3. Infrastructure Tests (server/src/test/infrastructure/)
When to use: Testing complete business workflows and complex system scenarios
Naming convention:
- Simple features:
<feature>.test.ts - Complex features:
<feature>_<aspect>.test.ts(split by concern)
Examples:
projectManagement.test.tsbillingInvoiceGeneration_tax.test.tsbillingInvoiceGeneration_discounts.test.tsbillingInvoiceGeneration_edgeCases.test.tscreditExpirationCore.test.ts
Characteristics:
- Full system testing with real database
- Test complete business workflows (billing cycles, invoice generation, etc.)
- Use
TestContext.createHelpers()for comprehensive setup - Long-running tests (5-30 seconds per test)
- Often require seed data and complex fixtures
- Split large test suites by aspect using underscore notation
Database bring-up pattern (Billing suites):
- Override pgbouncer defaults to connect directly to PostgreSQL in test runs (
process.env.DB_PORT = '5432'and remapDB_HOSTtolocalhostwhen necessary). - Use the shared context harness:
const { beforeAll, beforeEach, afterEach, afterAll } = TestContext.createHelpers(); - In
beforeAll, callsetupContext({ runSeeds: true, cleanupTables: [...] })to provision the tenant, preload reference data, and register table cleanups. - Refresh the scoped context in
beforeEachviaresetContext(), then reseed tenant-scoped data (e.g., tax regions, numbering seeds) needed for each test. - Roll back with
rollbackContext()inafterEachand tear everything down withcleanupContext()inafterAllso temporary schemas/tables are dropped cleanly.
When to split tests:
- Test file exceeds 500 lines
- Multiple distinct concerns (tax, discounts, edge cases, etc.)
- Different setup requirements per concern
- Example: Invoice generation split into
_tax,_discounts,_subtotal,_edgeCases,_consistency
4. E2E Tests (server/src/test/e2e/)
When to use: Testing complete user workflows, API endpoints, and system integration
Naming convention: <feature>.e2e.test.ts
Examples:
companies.e2e.test.ts- Company API endpointsemail-only.e2e.test.ts- Email workflowoauth-flow.test.ts- OAuth integration
Subdirectory organization:
api/- REST API endpoint tests- Follow pattern:
<resource>.e2e.test.ts - Example:
companies.e2e.test.ts,tickets.e2e.test.ts
- Follow pattern:
email-settings/- Email-specific workflow testsutils/- E2E utilities (e.g.,e2eTestSetup.ts,apiTestHelpers.ts)factories/- Test data factories for creating realistic test datafixtures/- Static test data files
Characteristics:
- Test complete API request/response cycles
- Authenticate with real API keys (via
x-api-keyheader) - Test full workflows end-to-end
- Use
setupE2ETestEnvironment()helper - Moderate to long execution time (5-30 seconds per test)
- Test authentication, authorization, validation, error handling
5. Temporal Workflow Tests (ee/temporal-workflows/src/__tests__/)
When to use: Testing Temporal workflows and activities (Enterprise Edition only)
Naming convention:
- E2E:
<workflow>.e2e.test.ts - Activities:
<activity>.test.tsor<activity>.temporal.test.ts - Standalone:
<activity>-standalone.test.ts
Examples:
email-only.e2e.test.tstenant-creation-workflow.e2e.test.tsemail-activities.temporal.test.tsemail-activities-standalone.test.ts
Subdirectory organization:
e2e/- Full workflow integration testsactivities/__tests__/- Activity unit and integration tests
6. Playwright Tests (Browser E2E)
When to use: Testing browser-based user interactions (currently limited use)
Location: ee/server/src/__tests__/integration/
Configuration: playwright.config.ts
Note: Playwright is configured but not widely used. Most E2E testing uses Vitest.
Playwright E2E (EE) With Fresh DB
For Enterprise Playwright browser tests (VS Code runner and CLI), we keep a clean, migrated, and seeded database at test-session startup. This avoids flakiness and makes tests deterministic.
- Where:
ee/server/src/__tests__/integration/** - Config:
ee/server/playwright.config.ts,ee/server/playwright.global-setup.ts - Bootstrap:
scripts/bootstrap-playwright-db.ts
How It Works
- Fresh DB per Playwright session, before the web server starts:
ee/server/playwright.config.tsruns the DB bootstrap script inwebServer.commandbeforenpm run dev:cd ../../ && node --import tsx/esm scripts/bootstrap-playwright-db.ts && NEXT_PUBLIC_EDITION=enterprise npm run dev
- In VS Code Playwright UI mode this runs once per “Restart” of the runner (session start). Click “Restart” in the Playwright panel to rebuild the DB.
- The bootstrap script:
- Drops and recreates the Playwright test database, runs all migrations and seeds.
- Provisions
app_userand grants privileges. - Reads credentials from
server/.env(override withPLAYWRIGHT_DB_*).
- globalSetup applies Playwright DB env so the dev server connects to the right DB.
Authentication in Playwright (avoiding login flakiness)
- Problem: Interactive login during Playwright runs is slow and brittle (middleware redirects, cookie domain/host, edge-runtime session token decoding, etc.).
- Solution: For UI automation we bypass interactive auth and keep routes accessible in test runs:
- Add a targeted bypass in middleware for MSP routes when
E2E_AUTH_BYPASS=true.- Code:
server/src/middleware.tsguards/msp/*withif (process.env.E2E_AUTH_BYPASS === 'true') return NextResponse.next().
- Code:
- Enable the bypass only for Playwright’s dev server:
ee/server/playwright.config.tssetsenv: { E2E_AUTH_BYPASS: 'true' }inwebServer.env.
- Keep prod/staging secure: do not set
E2E_AUTH_BYPASSoutside of test/dev.
- Add a targeted bypass in middleware for MSP routes when
- Why this approach:
- Eliminates cookie/host mismatch issues between
localhostvs.canonical.localhost. - Avoids edge vs. node token decoding differences.
- Focuses Playwright on UI correctness instead of auth.
- Eliminates cookie/host mismatch issues between
Notes
- If a specific test must exercise auth flows, disable bypass for that test run (unset
E2E_AUTH_BYPASS) and use a test-only auth helper to seed a valid session cookie on the expected host. - For API E2E tests prefer
x-api-keywith per-test keys rather than browser sessions. - When you need bespoke data, prefer hitting the actual product route and seed mocks through Playwright (for example via
page.addInitScriptto populatewindow.__ALGA_PLAYWRIGHT_*) instead of building a dedicated UI harness page. - To bypass the MSP login wall, reuse
applyPlaywrightAuthEnvDefaultsandsetupAuthenticatedSession(or mint your own cookie with@auth/core/jwt) fromee/server/src/__tests__/integration/helpers/playwrightAuthSessionHelper.ts.
Test File Pattern (seeded data; no DB writes)
- Use an admin Knex connection to query seed data for test setup (bypasses RLS/ACL).
- Do not create tenants/users in tests. Pick a seeded user and mint a valid Auth.js cookie.
Template
// ee/server/src/__tests__/integration/my-feature.playwright.test.ts
import { test, expect } from '@playwright/test';
import { encode } from '@auth/core/jwt';
import { knex as createKnex } from 'knex';
import { PLAYWRIGHT_DB_CONFIG } from './utils/playwrightDatabaseConfig';
function adminDb() {
return createKnex({
client: 'pg',
connection: {
host: PLAYWRIGHT_DB_CONFIG.host,
port: PLAYWRIGHT_DB_CONFIG.port,
database: PLAYWRIGHT_DB_CONFIG.database,
user: PLAYWRIGHT_DB_CONFIG.adminUser, // admin for read access to seeds
password: PLAYWRIGHT_DB_CONFIG.adminPassword,
},
pool: { min: 0, max: 5 },
});
}
async function getSeededUser(db, email?: string) {
if (email) {
const row = await db('users').where({ email: email.toLowerCase() }).first();
if (row) return row;
}
const any = await db('users').first();
if (!any) throw new Error('No seeded users found. Check seeds.');
return any;
}
async function setSessionCookie(page, user) {
const token = await encode({
token: {
sub: user.user_id,
id: user.user_id,
email: user.email,
tenant: user.tenant,
user_type: user.user_type || 'client',
},
secret: process.env.NEXTAUTH_SECRET!,
maxAge: 60 * 60,
salt: 'authjs.session-token',
});
await page.context().addCookies([{
name: 'authjs.session-token',
value: token,
url: 'http://localhost:3000',
httpOnly: true,
secure: false,
sameSite: 'Lax',
}]);
}
test.describe('My Feature', () => {
test.setTimeout(180_000); // allow time for first-run migrations
let db;
test.beforeAll(async () => { db = adminDb(); });
test.afterAll(async () => { await db?.destroy().catch(() => undefined); });
test('happy path', async ({ page }) => {
const seeded = await getSeededUser(db, process.env.CLIENT_PORTAL_TEST_EMAIL);
await setSessionCookie(page, seeded);
await page.goto('http://localhost:3000/some/route');
await expect(page.getByText('Welcome')).toBeVisible();
});
});
Running
- VS Code Playwright panel:
- Click “Restart” to start a new session; the Runner executes the DB bootstrap and then starts Next. Subsequent runs reuse the server/DB until you restart.
- CLI:
npx playwright test ee/server/src/__tests__/integration/<file>.playwright.test.ts- The DB bootstrap runs before
npm run dev(per session).
Variations
- Reset every run: set
reuseExistingServer: falseinee/server/playwright.config.ts; the server restarts and the DB bootstrap re-runs on every test run. - Per‑fixture reset (advanced): add a dev-only reset endpoint and POST it from
beforeAll. Use only if you need frequent resets while keeping the server running.
File Naming Patterns
| Test Type | Pattern | Example |
|---|---|---|
| Unit | <feature>.test.ts |
bucketUsageService.test.ts |
| Unit (Component) | <ComponentName>.test.tsx |
EmailProviderConfiguration.test.tsx |
| Integration | <feature>Integration.test.ts |
bucketUsageIntegration.test.ts |
| Integration (Specific) | <feature><Purpose>.test.ts |
googleProviderDatabase.test.ts |
| Infrastructure | <feature>.test.ts |
projectManagement.test.ts |
| Infrastructure (Split) | <feature>_<aspect>.test.ts |
billingInvoiceGeneration_tax.test.ts |
| E2E | <feature>.e2e.test.ts |
companies.e2e.test.ts |
| Temporal E2E | <workflow>.e2e.test.ts |
email-only.e2e.test.ts |
| Temporal Activity | <activity>.temporal.test.ts |
email-activities.temporal.test.ts |
Test Organization Decision Tree
Is it testing a single function/class/component in isolation?
├─ YES → Unit Test (server/src/test/unit/)
│ ├─ Component? → server/src/test/unit/components/<Component>.test.tsx
│ ├─ App route? → server/src/test/unit/app/<path>/page.test.ts
│ └─ Service/Action? → server/src/test/unit/<feature>.test.ts
│
└─ NO → Does it test multiple components together?
├─ YES → Integration Test (server/src/test/integration/)
│ └─ server/src/test/integration/<feature>Integration.test.ts
│
└─ NO → Does it test complete business workflows?
├─ YES → Infrastructure Test (server/src/test/infrastructure/)
│ ├─ Simple? → server/src/test/infrastructure/<feature>.test.ts
│ └─ Complex? → Split by aspect:
│ ├─ server/src/test/infrastructure/<feature>_<aspect1>.test.ts
│ └─ server/src/test/infrastructure/<feature>_<aspect2>.test.ts
│
└─ NO → Does it test API endpoints or complete user flows?
└─ YES → E2E Test (server/src/test/e2e/)
├─ API? → server/src/test/e2e/api/<resource>.e2e.test.ts
└─ Workflow? → server/src/test/e2e/<workflow-name>/<feature>.test.ts
Continuous Integration
Test execution in CI is tiered to keep PR feedback fast while still exercising the expensive suites regularly:
| Workflow | Trigger | What runs | Database |
|---|---|---|---|
unit-tests.yml |
every PR, push to main | nx affected -t test (DB-free unit suites across projects with a test target, Nx-cached, two-pass to avoid runner OOM) + skipped-test budget guard |
none |
unit-tests.yml (coverage job) |
push to main only | server unit suite with coverage, lcov uploaded as artifact (informational) | none |
integration-tests.yml |
PRs touching server/packages/shared/ee | Tier-1 subset: server npm run test:integration:tier1 (billing, accounting, authorization) |
Postgres service container (pgvector) |
integration-tests.yml |
nightly cron, manual dispatch (suite: full) |
full server/src/test/integration suite |
Postgres service container |
e2e-fresh-install-tests.yaml |
PRs (path-filtered), push to main | full docker-compose stack + Playwright | full stack |
Notes:
- CI sets
REQUIRE_DB=1, which makesdescribeWithDb()(server/test-utils/requireDb.ts) throw instead of skipping when Postgres is unreachable. Never reintroduce silentdbReachable ? describe : describe.skipprobes — a DB outage in CI must fail. - CI pins the vitest shuffle seed via
VITEST_SEEDso order-dependent failures reproduce across reruns. - Skipped tests are budgeted:
scripts/check-skip-budget.mjsfails CI when the number of.skip/xit/xdescribemarkers exceedsskip-budget.json. Lower the budget when you fix skips; raising it requires editing the budget file in your PR. - New workflows start as non-required checks; they are promoted to required in branch protection after a green shakeout period.
Coverage Roadmap (priority order)
Seeded suites exist for authorization, tenancy, job handlers, inbound webhooks, and the billing money paths. Highest-value areas still needing depth:
- Invoice generation & recurring billing (
packages/billing/src/actions/) — extend the seeded integration suites - Payments (
paymentActions.ts,paymentWebhookHelpers.ts,ee/server/src/lib/payments) - Tax (
taxService.ts, tax actions) and accounting export (accountingExportService.ts, companySync adapters) - Integrations mappers (QuickBooks, Xero, CSV in
packages/integrations) - API route contract tests (
server/src/app/api/v1/*) — auth required, input validation, error shapes shared/billingClients(54 files, ~1 test)- Email (
packages/email), reports (server/src/lib/reports), EE integrations (Tactical RMM, NinjaOne)
When adding tests to a package that has none, give it a vitest.config.ts
(model: packages/tickets/vitest.config.ts) and a "test": "vitest run" script
so nx affected -t test picks it up in CI.
Running Tests
NPM Scripts
# All tests
npm test
# By type
npm run test:unit # Unit tests only
npm run test:integration # Integration tests only
npm run test:infrastructure # Infrastructure tests only
npm run test:e2e # E2E tests only
# Specific suites
npm run test:e2e:email-settings # Email E2E tests
# Watch mode
npm run test:watch # Run tests in watch mode
# Local with config
npm run test:local # Run with local config
### Runner backend smoke tests
```bash
# Validate runner backend selection logic (Knative vs Docker)
npx vitest run ee/server/src/lib/extensions/__tests__/runner/backend.test.ts
# Spin up the local Docker runner (requires Docker)
npm run runner:up
# Check runner health and bundle-store wiring
curl http://localhost:${RUNNER_DOCKER_PORT:-8085}/healthz
# Fetch a static asset once a bundle is staged
curl -I "http://localhost:${RUNNER_DOCKER_PORT:-8085}/ext-ui/<extensionId>/<contentHash>/index.html"
# Tear down the runner stack
npm run runner:down
Use .env.runner (see .env.runner.example) to point the runner container at your bundle storage (MinIO/S3) and registry endpoints when validating the compose stack.
### Vitest Configuration
Tests use Vitest as the primary test runner, configured in `server/vitest.config.ts`:
- **Environment:** Node (default), jsdom (for React components)
- **Setup files:** `./src/test/setup.ts`
- **Global setup:** `./vitest.globalSetup.js`
- **Execution:** Single fork mode (for database isolation)
- **Timeout:** 20 seconds default
## Test File Templates
### Unit Test Template
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { functionUnderTest } from '@/lib/services/myService';
describe('MyService Unit Tests', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('functionUnderTest', () => {
it('should handle expected input correctly', () => {
// Arrange
const input = 'test';
// Act
const result = functionUnderTest(input);
// Assert
expect(result).toBe('expected');
});
it('should throw error for invalid input', () => {
// Arrange & Act & Assert
expect(() => functionUnderTest(null)).toThrow();
});
});
});
React Component Unit Test Template
/**
* @vitest-environment jsdom
*/
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import '@testing-library/jest-dom';
import { MyComponent } from '../../../components/MyComponent';
// Mock dependencies
vi.mock('../../../lib/actions/myActions', () => ({
myAction: vi.fn(),
}));
describe('MyComponent', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should render with default props', () => {
render(<MyComponent />);
expect(screen.getByText('Expected Text')).toBeInTheDocument();
});
it('should handle user interaction', async () => {
const user = userEvent.setup();
render(<MyComponent />);
await user.click(screen.getByRole('button', { name: /click me/i }));
await waitFor(() => {
expect(screen.getByText('Success')).toBeInTheDocument();
});
});
});
Integration Test Template
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { createTenantKnex } from 'server/src/lib/db';
import { Knex } from 'knex';
import { myService } from 'server/src/lib/services/myService';
describe('MyService Integration Tests', () => {
let knex: Knex;
let tenant: string;
beforeAll(async () => {
const { knex: testKnex, tenant: testTenant } = await createTenantKnex();
knex = testKnex;
tenant = testTenant || 'default-test-tenant';
});
afterAll(async () => {
if (knex) {
await knex.destroy();
}
});
beforeEach(async () => {
// Clean up test data
await knex('test_table').where('tenant', tenant).del();
});
describe('myService function', () => {
it('should persist data to database correctly', async () => {
// Arrange
const testData = { name: 'test' };
// Act
await myService.create(testData);
// Assert
const result = await knex('test_table')
.where('tenant', tenant)
.first();
expect(result.name).toBe('test');
});
});
});
Infrastructure Test Template
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { TestContext } from '../../test-utils/testContext';
import { generateInvoice } from 'server/src/lib/actions/invoiceGeneration';
describe('Invoice Generation Infrastructure Tests', () => {
const testHelpers = TestContext.createHelpers();
let context: TestContext;
beforeAll(async () => {
context = await testHelpers.beforeAll({
runSeeds: true,
cleanupTables: [
'invoice_items',
'invoices',
'time_entries',
'company_contract_lines'
],
companyName: 'Test Company',
userType: 'internal'
});
});
afterAll(async () => {
await testHelpers.afterAll();
});
describe('Complete Invoice Generation Workflow', () => {
it('should generate invoice with correct tax calculations', async () => {
// Arrange - Create complete test scenario
const company = await context.createEntity('companies', {
company_name: 'Test Company'
});
// Act - Execute business workflow
const invoice = await generateInvoice(company.company_id);
// Assert - Verify complete workflow results
expect(invoice.total).toBeGreaterThan(0);
expect(invoice.tax_amount).toBeDefined();
});
});
});
E2E Test Template
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { setupE2ETestEnvironment, E2ETestEnvironment } from '../utils/e2eTestSetup';
describe('Companies API E2E Tests', () => {
let env: E2ETestEnvironment;
let createdCompanyIds: string[] = [];
beforeAll(async () => {
env = await setupE2ETestEnvironment({
companyName: 'E2E Test Company',
userName: 'e2e_test_user'
});
});
afterAll(async () => {
// Clean up created resources
for (const id of createdCompanyIds) {
try {
await env.apiClient.delete(`/api/v1/companies/${id}`);
} catch (error) {
// Ignore cleanup errors
}
}
await env.cleanup();
});
describe('Authentication', () => {
it('should reject requests without API key', async () => {
const response = await env.apiClient.get('/api/v1/companies');
expect(response.status).toBe(401);
});
});
describe('CRUD Operations', () => {
it('should create a company', async () => {
const response = await env.apiClient.post('/api/v1/companies', {
company_name: 'New Company'
});
expect(response.status).toBe(201);
expect(response.data.company_name).toBe('New Company');
createdCompanyIds.push(response.data.company_id);
});
});
});
Key Principles
1. Tests Should NOT Be Colocated by Default
- Use centralized test directories (
server/src/test/) - Only colocate in rare cases where test is deeply coupled to implementation
- Exception: Small utility functions that are rarely changed
2. Mirror Source Structure Within Test Directories
- Use subdirectories to organize by feature area
- Example:
test/unit/components/for component tests - Example:
test/e2e/api/for API endpoint tests
3. Use Clear, Descriptive File Names
- Include
.e2esuffix for E2E tests - Include
Integrationor<Purpose>for integration tests - Split complex features with underscore:
feature_aspect.test.ts
4. Split Large Test Suites
- When test file exceeds 500 lines, split by concern
- Use underscore naming:
billingInvoiceGeneration_tax.test.ts - Keep related tests together:
feature_core.test.ts,feature_edgeCases.test.ts
5. Follow the Test Type Hierarchy
- Start with unit tests (fast, isolated)
- Add integration tests (multi-component)
- Use infrastructure tests for complete workflows
- E2E tests for full API/user flows
6. Use Appropriate Test Utilities
- Unit:
vi.mock()for mocking dependencies - Integration/Infrastructure:
TestContext.createHelpers()for database setup - E2E:
setupE2ETestEnvironment()for full environment setup
7. Database Testing Best Practices
- Always filter by tenant in all queries
- Clean up test data in
afterEachorafterAll - Use transactions where possible for isolation
- Ensure tests can run in any order
- For REST/E2E suites that need real HTTP semantics:
- Spin up a lightweight HTTP server (e.g. Node's
http.createServer) that delegates incoming requests to the Next.js route handlers usingNextRequest. - Use
ApiTestClient(or similar API helper) to exercise the endpoints the same way the product does, including headers, auth, and query parameters. - Manage the server lifecycle in
beforeAll/afterAllso the listener is available across tests and shut down cleanly.
- Spin up a lightweight HTTP server (e.g. Node's
- When the test flow requires overlaid storage/migration behavior without a running application:
- Ensure required tables exist by inspecting the schema and creating or altering tables as needed inside the test setup.
- Create or update database roles (e.g.
app_user) and grant privileges so the real connection pool can authenticate exactly as the application would.
- For isolation between tests while still hitting the live handlers:
- Use
TestContext.createHelpers()to wrap each test in a database transaction; perform per-test seeding afterbeforeEach()so every test sees a clean, consistent state. - If tests need to override service configuration (quotas, limits, etc.), use
vi.spyOnto swap implementations temporarily, and restore them infinallyblocks.
- Use
8. Mock External Dependencies
- Mock external APIs (email providers, payment gateways, etc.)
- Use
vi.mock()for module-level mocks - Create reusable mock fixtures in
test/mocks/
9. Test Naming Conventions
- Describe what is being tested:
describe('MyService') - Use "should" statements:
it('should return correct value') - Be specific about scenarios:
it('should throw error for invalid input')
10. Assertion Best Practices
- Use descriptive assertions:
expect(result.name).toBe('expected') - Test both happy path and error cases
- Verify side effects (database changes, API calls, etc.)
- Use
toThrow()for error testing
11. Use Updated Billing Terminology
- Refer to billing
contract linesandcontractsinstead of the legacyplansandbundles. - When bringing in legacy helpers (e.g.,
createFixedPlanAssignment), alias them to the new naming in your test files so intent stays aligned with the schema.
Additional Resources
- Vitest Documentation
- Testing Library Documentation
- Contact API E2E Test Plan - Example E2E test implementation
- Inbound Email Testing Guide - Email workflow testing examples