Skip to main content

Testing Strategy

Related docs: SDLC/TESTING · CI/CD Pipeline · Microservices


1. Testing Pyramid

Pakashop employs a comprehensive testing strategy across all layers of the platform:

/\
/ \ E2E Tests (Playwright)
/----\
/ \ Integration Tests (Jest, Supertest, httpx)
/--------\ Unit Tests (Jest, pytest, Go test)
/----------\ Component Tests (React Testing Library)
/------------\ Performance Tests (k6)
/--------------\ Security Tests (OWASP ZAP, npm audit)
/----------------\

2. Test Layers

2.1 Unit Tests

Service TypeFrameworkCoverage TargetRun Command
Node.jsJest90% utilities, 80% servicesnpm run test:unit
GoGo test80%go test ./...
Pythonpytest80%pytest
FrontendJest70% componentsnpm run test

Example: Node.js Service Unit Test

// services/backend/src/services/payment/__tests__/payment.service.test.js
describe('PaymentService', () => {
describe('calculateSettlement', () => {
it('calculates vendor amount and platform fee correctly', () => {
const item = { price: 100.00, quantity: 2 };
const result = PaymentService.calculateSettlement(item, 0.05);

expect(result.vendorAmount).toBe(190.00);
expect(result.platformFee).toBe(10.00);
});

it('applies VAT correctly', () => {
const item = { price: 100.00, quantity: 1 };
const result = PaymentService.calculateSettlement(item, 0.05);

expect(result.vat).toBe(16.00);
expect(result.total).toBe(116.00);
});
});
});

2.2 Integration Tests

Service TypeFrameworkCoverage Target
Node.jsJest + Supertest100% happy path + key error paths
GoGo test + httptest100% happy path + key error paths
Pythonpytest + httpx100% happy path + key error paths

Example: Payment Integration Test

// services/backend/src/__tests__/payments.integration.test.js
describe('POST /api/v1/payments/initiate', () => {
it('returns ussd response for MTN_MONEY', async () => {
process.env.PAYMENT_PROVIDER = 'MOCK';
const { body } = await request(app)
.post('/api/v1/payments/initiate')
.set('Authorization', `Bearer ${testUserToken}`)
.set('x-pakashop-key', process.env.PAKASHOP_CLIENT_KEY)
.send({
orderId: testOrderId,
paymentMethod: 'MTN',
phoneNumber: '0977123456'
})
.expect(200);

expect(body.success).toBe(true);
expect(body.data.transactionId).toBeDefined();
expect(body.data.message).toMatch(/payment request/i);
});

it('blocks payment when fraud score exceeds threshold', async () => {
const { body } = await request(app)
.post('/api/v1/payments/initiate')
.set('Authorization', `Bearer ${testUserToken}`)
.send({ orderId: suspiciousOrderId, paymentMethod: 'MTN' })
.expect(403);

expect(body.code).toBe('FRAUD_001');
});
});

2.3 Component Tests

// frontend/src/components/checkout/__tests__/PaymentMethodSelector.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import PaymentMethodSelector from '../PaymentMethodSelector';

describe('PaymentMethodSelector', () => {
it('renders all payment methods', () => {
render(<PaymentMethodSelector selectedMethod="MTN_MONEY" onSelect={jest.fn()} />);

expect(screen.getByText('MTN Mobile Money')).toBeInTheDocument();
expect(screen.getByText('Airtel Money')).toBeInTheDocument();
expect(screen.getByText('Zamtel Kwacha')).toBeInTheDocument();
expect(screen.getByText('Visa / Mastercard')).toBeInTheDocument();
});

it('calls onSelect when a method is clicked', () => {
const onSelect = jest.fn();
render(<PaymentMethodSelector selectedMethod="MTN_MONEY" onSelect={onSelect} />);

fireEvent.click(screen.getByText('Airtel Money'));
expect(onSelect).toHaveBeenCalledWith('AIRTEL_MONEY');
});
});

2.4 End-to-End Tests (Playwright)

// tests/e2e/checkout.spec.js
import { test, expect } from '@playwright/test';

test('complete mobile money checkout', async ({ page }) => {
await page.goto('/checkout');

// Fill delivery form
await page.fill('[name="firstName"]', 'John');
await page.fill('[name="lastName"]', 'Banda');
await page.fill('[name="email"]', 'john@example.zm');
await page.fill('[name="phone"]', '0977123456');
await page.fill('[name="address"]', 'Plot 123, Cairo Road');
await page.selectOption('[name="province"]', 'Lusaka');

await page.click('button[type="submit"]');

// Select payment method
await page.click('text=MTN Mobile Money');
await page.fill('[name="mobileWalletNumber"]', '0977123456');
await page.check('[name="consent"]');

await page.click('button:has-text("Place Order & Pay")');

// Wait for USSD overlay
await expect(page.locator('[data-testid="ussd-overlay"]')).toBeVisible();

// Simulate payment completion (mock)
await page.waitForTimeout(3000);
await expect(page).toHaveURL(/\/orders\//);
});

2.5 Performance Tests (k6)

// tests/performance/search.js
import http from 'k6/http';
import { check } from 'k6';

export const options = {
stages: [
{ duration: '1m', target: 100 },
{ duration: '3m', target: 100 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<100'],
http_req_failed: ['rate<0.01'],
},
};

export default function () {
const res = http.get('https://staging.pakashop.store/api/v1/products?search=phone');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 100ms': (r) => r.timings.duration < 100,
});
}

2.6 Security Tests

OWASP ZAP Baseline Scan:

# .github/workflows/security-scan.yml
name: Security Scan
on:
schedule:
- cron: '0 0 * * 0'

jobs:
zap-baseline:
runs-on: ubuntu-latest
steps:
- name: OWASP ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'https://staging.pakashop.store'
rules_file_name: '.zap/rules.tsv'

Dependency Audit:

npm audit --audit-level=moderate
# Block PRs with high/critical vulnerabilities

3. Test Environment

EnvironmentDatabaseGatewayRedisMeilisearchPurpose
LocalLocal PostgreSQL (Docker)MOCKLocal RedisLocal MeilisearchDeveloper testing
CI (GitHub Actions)PostgreSQL service containerMOCKRedis service containerMeilisearch service containerAutomated tests per PR
StagingStaging PostgreSQLPawaPay/Flutterwave sandboxStaging RedisStaging MeilisearchPre-release QA
ProductionProduction PostgreSQLPawaPay/Flutterwave liveProduction RedisProduction MeilisearchLive platform

3.1 CI Service Containers

services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: test, POSTGRES_DB: pakashop_test }
ports: ['5432:5432']
redis:
image: redis:7
ports: ['6379:6379']
meilisearch:
image: getmeili/meilisearch:v1.7
env: { MEILI_MASTER_KEY: test_key }
ports: ['7700:7700']

3.2 Test Database

  • Database: pakashop_test
  • Redis DB: 1
  • Test ports: 4000-series (e.g., backend test on 4001, gateway test on 4000)
  • Reset before each test run: prisma migrate reset --force

4. Coverage Requirements

LayerMinimum Coverage
Backend utilities (utils/)90%
Service layer (services/)80%
Route handlers (routes/)100% happy path + key error paths
Payment service (services/payment/)100% (all provider branches, all webhook events)
Fraud detection (services/fraud/)100% (all rule branches, scoring logic)
Frontend components70%
Go services (search, analytics)80%
Python services (moderation, recommendations)80%

5. Continuous Testing

  • PR CI: All unit and integration tests run automatically on every pull request.
  • Production Gate: Merges to production require the full test suite to pass.
  • Weekly E2E: Playwright suite runs weekly on staging.
  • Weekly Performance: k6 tests run weekly on staging.
  • Weekly Security: OWASP ZAP scan and npm audit run weekly.
  • Nightly: Full regression test suite on staging.

6. Test Data Management

  • prisma/seed.ts provides consistent test data (users, shops, products, orders).
  • Test database reset before each integration test run.
  • No production data used in tests; synthetic Zambian data (e.g., 0977000001 for test phones).
  • Test Redis uses DB 1 to avoid conflicts.
  • Test services run on 4000-series ports.

For internal use only. Do not distribute outside Pakashop engineering.