Testing Strategy
Test Pyramid
┌─────────┐
/ E2E \ 5% — Critical user flows
/ Tests \
/──────────────\
/ Integration \ 25% — API endpoints, DB queries
/ Tests \
/────────────────────\
/ Unit Tests \ 70% — Domain logic, utilities
/────────────────────────\Test Structure
apps/api/
├── src/
│ └── modules/
│ └── mlm/
│ └── commission/
│ ├── domain/
│ │ └── services/
│ │ ├── commission-calculator.service.ts
│ │ └── commission-calculator.service.spec.ts ← Unit test
│ └── application/
│ └── commands/
│ ├── calculate-commission.handler.ts
│ └── calculate-commission.handler.spec.ts ← Unit test
├── test/
│ ├── integration/ ← Integration tests
│ │ ├── auth.e2e-spec.ts
│ │ ├── mlm.e2e-spec.ts
│ │ └── orders.e2e-spec.ts
│ ├── e2e/ ← End-to-end tests
│ │ └── checkout-flow.e2e-spec.ts
│ ├── fixtures/ ← Test data factories
│ │ ├── user.fixture.ts
│ │ ├── partner.fixture.ts
│ │ └── order.fixture.ts
│ └── setup/
│ ├── test-database.ts ← Test DB setup
│ └── mock-providers.ts ← External service mocksTesting Tools
| Purpose | Tool |
|---|---|
| Test runner | Jest |
| HTTP testing | Supertest |
| Mocking | Jest mocks + custom factories |
| Coverage | Jest coverage |
| Database | Testcontainers (PostgreSQL) |
| Fixtures | Custom factory functions |
Unit Testing Rules
typescript
// Domain services MUST be framework-agnostic and easily testable
// ✅ Good: Pure domain logic
class CommissionCalculator {
calculate(amount: number, tiers: CommissionTier[]): CommissionResult {
// Pure logic, no dependencies
}
}
// ✅ Good: Dependencies injected via interfaces
class CommissionService {
constructor(
private readonly partnerRepo: IPartnerRepository, // Interface, not implementation
private readonly calculator: CommissionCalculator,
) {}
}
// ❌ Bad: Direct framework/infrastructure dependency
class CommissionService {
constructor(
private readonly prisma: PrismaClient, // Direct infrastructure
) {}
}Unit Test Example
typescript
// commission-calculator.service.spec.ts
describe('CommissionCalculator', () => {
let calculator: CommissionCalculator;
beforeEach(() => {
calculator = new CommissionCalculator();
});
describe('calculate', () => {
it('should calculate commission for single tier', () => {
const tiers = [{ levelDepth: 1, percentage: 10 }];
const result = calculator.calculate(1000, tiers);
expect(result.commissions).toHaveLength(1);
expect(result.commissions[0].amount).toBe(100);
});
it('should apply rank minimum requirement', () => {
const tiers = [{ levelDepth: 1, percentage: 10, minRankLevel: 3 }];
const partnerRank = 2;
const result = calculator.calculate(1000, tiers, partnerRank);
expect(result.commissions).toHaveLength(0);
});
it('should handle empty tiers gracefully', () => {
const result = calculator.calculate(1000, []);
expect(result.commissions).toHaveLength(0);
expect(result.totalCommission).toBe(0);
});
});
});Integration Test Example
typescript
// test/integration/mlm.e2e-spec.ts
describe('MLM Module (Integration)', () => {
let app: INestApplication;
let prisma: PrismaService;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(IPaymentGateway)
.useClass(MockPaymentGateway)
.overrideProvider(INotificationSender)
.useClass(MockNotificationSender)
.compile();
app = moduleRef.createNestApplication();
prisma = moduleRef.get(PrismaService);
await app.init();
});
beforeEach(async () => {
// Clean database between tests
await prisma.$executeRaw`TRUNCATE TABLE mlm.commission_transactions CASCADE`;
});
afterAll(async () => {
await app.close();
});
describe('POST /api/v1/mlm/commissions/calculate', () => {
it('should calculate commissions for order', async () => {
// Arrange
const sponsor = await createPartnerFixture(prisma);
const order = await createOrderFixture(prisma, { referringPartnerId: sponsor.id });
// Act
const response = await request(app.getHttpServer())
.post('/api/v1/mlm/commissions/calculate')
.send({ orderId: order.id })
.expect(201);
// Assert
expect(response.body.commissions).toHaveLength(1);
expect(response.body.commissions[0].partnerId).toBe(sponsor.id);
// Verify database state
const dbCommissions = await prisma.commissionTransaction.findMany({
where: { sourceId: order.id },
});
expect(dbCommissions).toHaveLength(1);
});
});
});Test Database Strategy
typescript
// test/setup/test-database.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
let container: StartedPostgreSqlContainer;
export async function setupTestDatabase() {
container = await new PostgreSqlContainer('postgres:15')
.withDatabase('iwm_test')
.withUsername('test')
.withPassword('test')
.start();
process.env.DATABASE_URL = container.getConnectionUri();
// Run migrations
execSync('npx prisma migrate deploy', { stdio: 'inherit' });
}
export async function teardownTestDatabase() {
await container.stop();
}
// jest.config.js
module.exports = {
globalSetup: './test/setup/global-setup.ts',
globalTeardown: './test/setup/global-teardown.ts',
};Mock Providers
typescript
// test/setup/mock-providers.ts
export class MockPaymentGateway implements IPaymentGateway {
private responses = new Map<string, PaymentResult>();
// Configure mock responses for tests
mockPaymentResult(intentId: string, result: PaymentResult) {
this.responses.set(intentId, result);
}
async createPaymentIntent(params: CreatePaymentParams): Promise<PaymentIntent> {
return {
id: `pi_test_${Date.now()}`,
amount: params.amount,
currency: params.currency,
status: 'PENDING',
};
}
async confirmPayment(intentId: string): Promise<PaymentResult> {
return this.responses.get(intentId) ?? { success: true, transactionId: 'txn_test' };
}
}
export class MockNotificationSender implements INotificationSender {
public sentEmails: EmailParams[] = [];
public sentSMS: SMSParams[] = [];
async sendEmail(params: EmailParams): Promise<SendResult> {
this.sentEmails.push(params);
return { success: true, messageId: `msg_${Date.now()}` };
}
async sendSMS(params: SMSParams): Promise<SendResult> {
this.sentSMS.push(params);
return { success: true, messageId: `msg_${Date.now()}` };
}
}Coverage Targets
| Module | Target | Rationale |
|---|---|---|
| Domain services | 90%+ | Core business logic |
| Application commands/queries | 80%+ | Use case orchestration |
| Infrastructure | 60%+ | Integration points |
| Presentation (controllers) | 70%+ | API contracts |
| Overall | 75%+ | Reasonable for MVP |
What to Test
| Layer | What to Test | What NOT to Test |
|---|---|---|
| Domain | Business rules, calculations, validations | — |
| Application | Command/query orchestration, event handling | External service calls (mock them) |
| Infrastructure | Repository queries, mapper transformations | Framework internals |
| Presentation | Request validation, response format, auth guards | NestJS decorators themselves |