Skip to content

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 mocks

Testing Tools

PurposeTool
Test runnerJest
HTTP testingSupertest
MockingJest mocks + custom factories
CoverageJest coverage
DatabaseTestcontainers (PostgreSQL)
FixturesCustom 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

ModuleTargetRationale
Domain services90%+Core business logic
Application commands/queries80%+Use case orchestration
Infrastructure60%+Integration points
Presentation (controllers)70%+API contracts
Overall75%+Reasonable for MVP

What to Test

LayerWhat to TestWhat NOT to Test
DomainBusiness rules, calculations, validations
ApplicationCommand/query orchestration, event handlingExternal service calls (mock them)
InfrastructureRepository queries, mapper transformationsFramework internals
PresentationRequest validation, response format, auth guardsNestJS decorators themselves