Testing Strategy
Testing approach, coverage targets, and tools
Testing Strategy
Coverage Targets
| Component | Minimum Coverage |
|---|---|
| Services | 90% |
| Repositories | 80% |
| Controllers | 70% |
| Components | 70% |
| Overall | 80% |
Test Distribution
| Type | Proportion | Purpose |
|---|---|---|
| Unit Tests | 70% | Individual functions, services |
| Integration Tests | 20% | API endpoints, DB operations |
| E2E Tests | 10% | Critical user flows |
Testing Tools
Backend (NestJS)
- Jest - Test runner
- Supertest - HTTP testing
- Prisma mock - Database mocking
Frontend (Next.js)
- Jest - Test runner
- React Testing Library - Component testing
- Playwright - E2E browser testing
What to Test
Must Test
- Business logic in services
- API endpoint responses
- Authentication flows
- Permission checks (RBAC)
- Tenant isolation (critical!)
- Form validation
Can Skip
- Simple CRUD pass-through
- Framework internals
- Third-party library code
Test Structure
backend/test/
├── unit/ # Service, repository tests
├── integration/ # API endpoint tests
└── e2e/ # Full flow tests
frontend/__tests__/
├── unit/ # Component, hook tests
├── integration/ # Page tests with mocked API
└── e2e/ # Playwright browser testsCritical Tests
Tenant Isolation
Every feature must verify:
- User A in Tenant 1 cannot see Tenant 2 data
- Queries always include
tenantIdfilter - Cross-tenant access returns 403
Authentication
- Login/logout flows work
- JWT tokens validate correctly
- Expired tokens rejected
- Invalid credentials handled
Permissions
- Platform admin can access all tenants
- Tenant admin limited to own tenant
- User permissions enforced on endpoints
Best Practices
- Test behavior, not implementation
- Use factories for test data
- Clean up after each test
- Mock external services
- Use
data-testidfor E2E selectors - Run tests in CI on every PR
Test Data Factories
Use factories to create consistent test data:
// test/factories/employee.factory.ts
import { faker } from '@faker-js/faker'
import { Employee, EmployeeStatus } from '@prisma/client'
export function createTestEmployee(overrides: Partial<Employee> = {}): Omit<Employee, 'id' | 'createdAt' | 'updatedAt'> {
return {
tenantId: overrides.tenantId ?? faker.string.uuid(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: faker.internet.email(),
employeeNumber: faker.string.alphanumeric(6).toUpperCase(),
status: EmployeeStatus.ACTIVE,
hireDate: faker.date.past(),
departmentId: null,
managerId: null,
deletedAt: null,
...overrides,
}
}
export function createTestTenant(overrides = {}) {
return {
name: faker.company.name(),
domain: faker.internet.domainName(),
status: 'ACTIVE',
...overrides,
}
}Prisma Mocking for Unit Tests
// test/mocks/prisma.mock.ts
import { PrismaClient } from '@prisma/client'
import { mockDeep, DeepMockProxy } from 'jest-mock-extended'
export type MockPrismaClient = DeepMockProxy<PrismaClient>
export const createMockPrisma = (): MockPrismaClient => mockDeep<PrismaClient>()
// Usage in test file
describe('EmployeeService', () => {
let service: EmployeeService
let prisma: MockPrismaClient
beforeEach(() => {
prisma = createMockPrisma()
service = new EmployeeService(prisma)
})
it('should find employee by id', async () => {
const mockEmployee = createTestEmployee({ id: 'emp-123' })
prisma.employee.findFirst.mockResolvedValue(mockEmployee as any)
const result = await service.findById('emp-123', 'tenant-1')
expect(result).toEqual(mockEmployee)
expect(prisma.employee.findFirst).toHaveBeenCalledWith({
where: { id: 'emp-123', tenantId: 'tenant-1' },
})
})
})Integration Test Setup
// test/integration/setup.ts
import { PrismaClient } from '@prisma/client'
import { createTestTenant, createTestEmployee } from '../factories'
const prisma = new PrismaClient()
export async function setupTestDatabase() {
// Clean up before tests
await prisma.employee.deleteMany()
await prisma.tenant.deleteMany()
// Seed base data
const tenant = await prisma.tenant.create({
data: createTestTenant({ id: 'test-tenant-1' }),
})
const admin = await prisma.employee.create({
data: createTestEmployee({
tenantId: tenant.id,
email: 'admin@test.com',
}),
})
return { tenant, admin }
}
export async function teardownTestDatabase() {
await prisma.$disconnect()
}Playwright E2E Test Structure
// e2e/employees.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Employee Management', () => {
test.beforeEach(async ({ page }) => {
// Login as admin
await page.goto('/login')
await page.fill('[data-testid="email-input"]', 'admin@test.com')
await page.fill('[data-testid="password-input"]', 'password')
await page.click('[data-testid="login-button"]')
await page.waitForURL('/dashboard')
})
test('should display employee list', async ({ page }) => {
await page.goto('/employees')
await expect(page.getByTestId('employee-list')).toBeVisible()
await expect(page.getByTestId('employee-row')).toHaveCount(10)
})
test('should create new employee', async ({ page }) => {
await page.goto('/employees/new')
await page.fill('[data-testid="first-name"]', 'John')
await page.fill('[data-testid="last-name"]', 'Doe')
await page.fill('[data-testid="email"]', 'john.doe@company.com')
await page.click('[data-testid="submit-button"]')
await expect(page.getByText('Employee created successfully')).toBeVisible()
})
})Running Tests
# Unit tests
npm test
# Integration tests
npm test:integration
# E2E tests
npm test:e2e
# Coverage report
npm test:coverage
# Watch mode
npm test:watchTest patterns to be refined during development