Testing Strategy
Testing approach and patterns for the CRM platform.
Test Pyramid
┌───────────────┐
│ E2E Tests │ (few, slow)
│ Playwright │
└───────────────┘
┌─────────────────────┐
│ Integration Tests │ (moderate)
│ Testcontainers │
└─────────────────────┘
┌───────────────────────────────────────┐
│ Unit Tests │ (many, fast)
│ Vitest │
└───────────────────────────────────────┘
Unit Tests
Tools
- Framework: Vitest
- Mocking: vi.mock, vi.fn
What to Unit Test
- Service business logic
- Utility functions
- DTOs and validation
- Guard and interceptor logic
Example
describe('IdentityResolutionService', () => {
let service: IdentityResolutionService;
let mockPrisma: DeepMockProxy<PrismaClient>;
beforeEach(() => {
mockPrisma = mockDeep<PrismaClient>();
service = new IdentityResolutionService(mockPrisma);
});
it('should match by email', async () => {
const existingProfile = { id: '123', email: 'test@example.com' };
mockPrisma.customerProfile.findFirst.mockResolvedValue(existingProfile);
const result = await service.resolve({
tenantId: 'tenant-1',
email: 'test@example.com',
});
expect(result).toEqual(existingProfile);
expect(mockPrisma.customerProfile.findFirst).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1', email: 'test@example.com' },
});
});
it('should create new profile when no match', async () => {
mockPrisma.customerProfile.findFirst.mockResolvedValue(null);
mockPrisma.customerProfile.create.mockResolvedValue({
id: 'new-123',
email: 'new@example.com',
});
const result = await service.resolveOrCreate({
tenantId: 'tenant-1',
email: 'new@example.com',
});
expect(result.id).toBe('new-123');
expect(mockPrisma.customerProfile.create).toHaveBeenCalled();
});
});
Integration Tests
Tools
- Framework: Vitest
- Database: Testcontainers (PostgreSQL)
- Queue: Testcontainers (Redis)
What to Integration Test
- Database queries (Prisma)
- Queue processing (BullMQ)
- Cross-service interactions
Example
describe('CustomerProfileService (integration)', () => {
let container: StartedPostgreSqlContainer;
let prisma: PrismaClient;
let service: CustomerProfileService;
beforeAll(async () => {
container = await new PostgreSqlContainer().start();
prisma = new PrismaClient({
datasources: { db: { url: container.getConnectionUri() } },
});
await prisma.$executeRaw`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
// Run migrations...
service = new CustomerProfileService(prisma);
});
afterAll(async () => {
await prisma.$disconnect();
await container.stop();
});
it('should create and retrieve profile', async () => {
const profile = await service.create({
tenantId: 'test-tenant',
email: 'test@example.com',
firstName: 'Test',
});
const retrieved = await service.findById(profile.id);
expect(retrieved).toEqual(profile);
});
});
E2E Tests
Tools
- Framework: Playwright
- API Testing: Built-in request context
What to E2E Test
- Critical user flows
- API contract validation
- Auth flows
Example
test('create and execute campaign', async ({ request }) => {
// Create segment
const segmentResponse = await request.post('/api/v1/segments', {
data: {
name: 'Test Segment',
type: 'DYNAMIC',
rules: [{ field: 'status', operator: 'eq', value: 'ACTIVE' }],
},
});
const segment = await segmentResponse.json();
// Create campaign
const campaignResponse = await request.post('/api/v1/campaigns', {
data: {
name: 'Test Campaign',
type: 'ONE_TIME',
segmentId: segment.data.id,
channels: ['EMAIL'],
},
});
const campaign = await campaignResponse.json();
expect(campaign.data.status).toBe('DRAFT');
// Schedule campaign
await request.post(`/api/v1/campaigns/${campaign.data.id}/schedule`);
// Verify status
const updated = await request.get(`/api/v1/campaigns/${campaign.data.id}`);
const updatedData = await updated.json();
expect(updatedData.data.status).toBe('SCHEDULED');
});
Running Tests
# Unit tests
npx nx test crm-core
npx nx test crm-campaigns
# Integration tests
npx nx test crm-backend --configuration=integration
# E2E tests
npx nx e2e crm-backend-e2e
# All tests with coverage
npx nx test crm-core --coverage
Test Fixtures
Factory Pattern
export const customerProfileFactory = {
build: (overrides?: Partial<CustomerProfile>): CustomerProfile => ({
id: faker.string.uuid(),
tenantId: 'test-tenant',
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
status: 'ACTIVE',
lifecycleStage: 'CUSTOMER',
...overrides,
}),
};
Mocking External Services
Messaging Service
const mockMessagingClient = {
upsertSegment: vi.fn().mockResolvedValue({ id: 'msg-segment-1' }),
sendCampaign: vi.fn().mockResolvedValue({ success: true }),
};
Social APIs
const mockFacebookClient = {
publishPost: vi.fn().mockResolvedValue({ id: 'fb-post-123' }),
getEngagement: vi.fn().mockResolvedValue({ likes: 10, shares: 5 }),
};
Coverage Targets
| Type | Target |
|---|---|
| Unit | 80% |
| Integration | 60% |
| E2E | Critical paths |