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 |