Skip to main content

Testing Strategy

This document outlines the testing approach for the CRM & Marketing Platform.


Testing Pyramid

                    /\
/ \
/ E2E \
/______\
/ \
/ Integr \
/____________\
/ \
/ Unit \
/__________________\

Unit Tests

Identity Resolution

describe('IdentityResolutionService', () => {
describe('resolve', () => {
it('should match by authUserId with 100% confidence', async () => {
// Test implementation
});

it('should match by email with 95% confidence', async () => {
// Test implementation
});

it('should normalize email to lowercase', async () => {
// Test implementation
});

it('should normalize SA phone numbers to E.164', async () => {
const cases = [
{ input: '082 123 4567', expected: '+27821234567' },
{ input: '0821234567', expected: '+27821234567' },
{ input: '+27821234567', expected: '+27821234567' },
];

for (const { input, expected } of cases) {
const result = service.normalizePhone(input);
expect(result).toBe(expected);
}
});

it('should create new profile when no match', async () => {
// Test implementation
});
});
});

Scoring Calculations

describe('EngagementScoringService', () => {
describe('calculateRecencyScore', () => {
it('should return 100 for activity within 7 days', () => {
const profile = { lastActivityAt: daysAgo(5) };
expect(service.calculateRecencyScore(profile)).toBe(100);
});

it('should return 0 for no activity', () => {
const profile = { lastActivityAt: null };
expect(service.calculateRecencyScore(profile)).toBe(0);
});
});

describe('calculateScore', () => {
it('should weight components correctly', async () => {
// Mock dependencies and verify weighting
});
});
});

Segment Rule Parsing

describe('SegmentationService', () => {
describe('buildWhereFromCriteria', () => {
it('should handle eq operator', () => {
const criteria = {
combinator: 'AND',
rules: [{ field: 'status', operator: 'eq', value: 'ACTIVE' }],
};

const where = service.buildWhereFromCriteria('tenant_1', criteria);

expect(where).toMatchObject({
tenantId: 'tenant_1',
status: 'ACTIVE',
});
});

it('should handle nested groups', () => {
const criteria = {
combinator: 'AND',
rules: [
{ field: 'status', operator: 'eq', value: 'ACTIVE' },
{
combinator: 'OR',
rules: [
{ field: 'tier', operator: 'eq', value: 'Gold' },
{ field: 'tier', operator: 'eq', value: 'Platinum' },
],
},
],
};

const where = service.buildWhereFromCriteria('tenant_1', criteria);

expect(where.OR).toBeDefined();
});
});
});

Journey Step Execution

describe('JourneyEngine', () => {
describe('executeWaitStep', () => {
it('should calculate correct wait time', async () => {
const cases = [
{ duration: '30 minutes', expectedMs: 30 * 60 * 1000 },
{ duration: '2 hours', expectedMs: 2 * 60 * 60 * 1000 },
{ duration: '3 days', expectedMs: 3 * 24 * 60 * 60 * 1000 },
];

for (const { duration, expectedMs } of cases) {
const now = Date.now();
const result = engine.calculateWaitUntil(duration);
const diff = result.getTime() - now;

expect(diff).toBeCloseTo(expectedMs, -3); // Within 1 second
}
});
});

describe('evaluateCondition', () => {
it('should evaluate AND combinator', () => {
const customer = { engagementScore: 80, emailOptIn: true };
const config = {
combinator: 'AND',
rules: [
{ field: 'engagementScore', operator: 'gte', value: 70 },
{ field: 'emailOptIn', operator: 'eq', value: true },
],
};

expect(engine.evaluateCondition(customer, config)).toBe(true);
});
});
});

Integration Tests

Event Processing

describe('CRM Event Processing', () => {
let app: INestApplication;
let prisma: PrismaService;
let queue: Queue;

beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [CrmModule],
}).compile();

app = module.createNestApplication();
await app.init();

prisma = module.get(PrismaService);
queue = module.get(getQueueToken('crm-events'));
});

afterAll(async () => {
await app.close();
});

it('should process member.created event end-to-end', async () => {
// Add event to queue
await queue.add('event', {
id: 'evt_123',
type: 'scl.member.created',
tenantId: 'tenant_1',
timestamp: new Date().toISOString(),
identity: {
sclMemberId: 12345,
email: 'test@example.com',
},
payload: {
firstName: 'Test',
lastName: 'User',
membershipTierName: 'Gold',
},
});

// Wait for processing
await waitForJob(queue);

// Verify profile created
const profile = await prisma.customerProfile.findFirst({
where: { sclMemberId: 12345 },
});

expect(profile).toBeDefined();
expect(profile.firstName).toBe('Test');
expect(profile.membershipTier).toBe('Gold');

// Verify activity created
const activity = await prisma.activity.findFirst({
where: { customerId: profile.id, type: 'MEMBER_CREATED' },
});

expect(activity).toBeDefined();
});
});

Campaign Execution

describe('Campaign Execution', () => {
it('should execute campaign and track interactions', async () => {
// Create segment
const segment = await segmentService.create({
tenantId: 'tenant_1',
name: 'Test Segment',
type: 'STATIC',
});

// Add members
await prisma.customerSegmentMembership.createMany({
data: testCustomers.map(c => ({
tenantId: 'tenant_1',
segmentId: segment.id,
customerId: c.id,
})),
});

// Create and execute campaign
const campaign = await campaignService.create({
tenantId: 'tenant_1',
name: 'Test Campaign',
segmentId: segment.id,
channels: ['EMAIL'],
emailTemplateId: 'template_1',
});

await campaignService.execute(campaign.id);

// Verify interactions recorded
const interactions = await prisma.campaignInteraction.findMany({
where: { campaignId: campaign.id },
});

expect(interactions.length).toBe(testCustomers.length);
expect(interactions.every(i => i.type === 'SENT')).toBe(true);
});
});

Journey Enrollment

describe('Journey Enrollment', () => {
it('should enroll customer and execute first step', async () => {
// Create journey with steps
const journey = await journeyService.create({
tenantId: 'tenant_1',
name: 'Test Journey',
triggerType: 'MANUAL',
});

await journeyService.addStep(journey.id, {
type: 'SEND',
name: 'Welcome Email',
config: { channel: 'EMAIL', templateId: 'welcome' },
});

await journeyService.activate(journey.id);

// Enroll customer
const enrollment = await journeyEngine.enrollCustomer(
journey.id,
testCustomer.id,
);

// Wait for step execution
await waitForJob(stepQueue);

// Verify enrollment
const updated = await prisma.journeyEnrollment.findUnique({
where: { id: enrollment.id },
include: { steps: true },
});

expect(updated.currentStepNumber).toBe(1);
expect(updated.steps[0].status).toBe('COMPLETED');
});
});

Load Tests

Identity Resolution

// tests/load/identity-resolution.ts

import http from 'k6/http';
import { check } from 'k6';

export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '1m', target: 100 },
{ duration: '30s', target: 0 },
],
};

export default function () {
const payload = {
tenantId: 'tenant_1',
email: `user${__VU}@example.com`,
phoneNumber: `+2782${String(__ITER).padStart(7, '0')}`,
};

const res = http.get(
`${__ENV.BASE_URL}/api/v1/customers/resolve?${new URLSearchParams(payload)}`
);

check(res, {
'status is 200': (r) => r.status === 200,
'response time < 100ms': (r) => r.timings.duration < 100,
});
}

Campaign Sending

// tests/load/campaign-sending.ts

export const options = {
scenarios: {
campaign: {
executor: 'shared-iterations',
vus: 10,
iterations: 1000,
},
},
};

export default function () {
// Simulate bulk message sending
}

Test Commands

# Unit tests
npx nx test crm-core
npx nx test crm-campaigns
npx nx test crm-automation

# Integration tests
npx nx test crm-backend --configuration=integration

# E2E tests
npx nx e2e crm-backend-e2e

# Load tests
k6 run tests/load/identity-resolution.ts

# Coverage
npx nx test crm-core --coverage