PSA/docs/reference/testing-standards.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

733 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Testing Standards
This document outlines testing conventions, file organization, and naming patterns for the Alga PSA codebase.
## Table of Contents
1. [Test Directory Structure](#test-directory-structure)
2. [Test Type Guidelines](#test-type-guidelines)
3. [File Naming Patterns](#file-naming-patterns)
4. [Test Organization Decision Tree](#test-organization-decision-tree)
5. [Running Tests](#running-tests)
6. [Test File Templates](#test-file-templates)
7. [Key Principles](#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 tests
- `auth.test.ts` - Authentication logic tests
- `EmailProviderConfiguration.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 jsdom` comment for React component tests
**Subdirectory organization:**
- `components/` - UI component tests
- `app/` - 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.ts`
- `emailProviderIntegration.test.ts`
- `googleProviderDatabase.test.ts`
**Characteristics:**
- Use real database connections via `createTenantKnex()`
- Test interactions between services, actions, and data layer
- Test data persistence and retrieval
- May use `TestContext` for 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.ts`
- `billingInvoiceGeneration_tax.test.ts`
- `billingInvoiceGeneration_discounts.test.ts`
- `billingInvoiceGeneration_edgeCases.test.ts`
- `creditExpirationCore.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 remap `DB_HOST` to `localhost` when necessary).
- Use the shared context harness: `const { beforeAll, beforeEach, afterEach, afterAll } = TestContext.createHelpers();`
- In `beforeAll`, call `setupContext({ runSeeds: true, cleanupTables: [...] })` to provision the tenant, preload reference data, and register table cleanups.
- Refresh the scoped context in `beforeEach` via `resetContext()`, then reseed tenant-scoped data (e.g., tax regions, numbering seeds) needed for each test.
- Roll back with `rollbackContext()` in `afterEach` and tear everything down with `cleanupContext()` in `afterAll` so 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 endpoints
- `email-only.e2e.test.ts` - Email workflow
- `oauth-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`
- `email-settings/` - Email-specific workflow tests
- `utils/` - E2E utilities (e.g., `e2eTestSetup.ts`, `apiTestHelpers.ts`)
- `factories/` - Test data factories for creating realistic test data
- `fixtures/` - Static test data files
**Characteristics:**
- Test complete API request/response cycles
- Authenticate with real API keys (via `x-api-key` header)
- 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.ts` or `<activity>.temporal.test.ts`
- Standalone: `<activity>-standalone.test.ts`
**Examples:**
- `email-only.e2e.test.ts`
- `tenant-creation-workflow.e2e.test.ts`
- `email-activities.temporal.test.ts`
- `email-activities-standalone.test.ts`
**Subdirectory organization:**
- `e2e/` - Full workflow integration tests
- `activities/__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.ts` runs the DB bootstrap script in `webServer.command` before `npm 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_user` and grants privileges.
- Reads credentials from `server/.env` (override with `PLAYWRIGHT_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.ts` guards `/msp/*` with `if (process.env.E2E_AUTH_BYPASS === 'true') return NextResponse.next()`.
- Enable the bypass only for Playwrights dev server:
- `ee/server/playwright.config.ts` sets `env: { E2E_AUTH_BYPASS: 'true' }` in `webServer.env`.
- Keep prod/staging secure: do not set `E2E_AUTH_BYPASS` outside of test/dev.
- Why this approach:
- Eliminates cookie/host mismatch issues between `localhost` vs. `canonical.localhost`.
- Avoids edge vs. node token decoding differences.
- Focuses Playwright on UI correctness instead of auth.
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-key` with 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.addInitScript` to populate `window.__ALGA_PLAYWRIGHT_*`) instead of building a dedicated UI harness page.
- To bypass the MSP login wall, reuse `applyPlaywrightAuthEnvDefaults` and `setupAuthenticatedSession` (or mint your own cookie with `@auth/core/jwt`) from `ee/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
```ts
// 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: false` in `ee/server/playwright.config.ts`; the server restarts and the DB bootstrap re-runs on every test run.
- Perfixture 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 makes `describeWithDb()` (server/test-utils/requireDb.ts)
**throw** instead of skipping when Postgres is unreachable. Never reintroduce
silent `dbReachable ? describe : describe.skip` probes a DB outage in CI must fail.
- CI pins the vitest shuffle seed via `VITEST_SEED` so order-dependent failures
reproduce across reruns.
- Skipped tests are budgeted: `scripts/check-skip-budget.mjs` fails CI when the
number of `.skip`/`xit`/`xdescribe` markers exceeds `skip-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:
1. Invoice generation & recurring billing (`packages/billing/src/actions/`) extend the seeded integration suites
2. Payments (`paymentActions.ts`, `paymentWebhookHelpers.ts`, `ee/server/src/lib/payments`)
3. Tax (`taxService.ts`, tax actions) and accounting export (`accountingExportService.ts`, companySync adapters)
4. Integrations mappers (QuickBooks, Xero, CSV in `packages/integrations`)
5. API route contract tests (`server/src/app/api/v1/*`) auth required, input validation, error shapes
6. `shared/billingClients` (54 files, ~1 test)
7. 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
```bash
# 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
```typescript
/**
* @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
```typescript
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
```typescript
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
```typescript
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 `.e2e` suffix for E2E tests
- Include `Integration` or `<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 `afterEach` or `afterAll`
- 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 using `NextRequest`.
- 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`/`afterAll` so the listener is available across tests and shut down cleanly.
- 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 after `beforeEach()` so every test sees a clean, consistent state.
- If tests need to override service configuration (quotas, limits, etc.), use `vi.spyOn` to swap implementations temporarily, and restore them in `finally` blocks.
### 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 lines` and `contracts` instead of the legacy `plans` and `bundles`.
- 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](https://vitest.dev/)
- [Testing Library Documentation](https://testing-library.com/)
- [Contact API E2E Test Plan](../archive/contact-api-e2e-test-plan.md) - Example E2E test implementation
- [Inbound Email Testing Guide](../inbound-email/development/testing.md) - Email workflow testing examples