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 Type | Framework | Coverage Target | Run Command |
|---|---|---|---|
| Node.js | Jest | 90% utilities, 80% services | npm run test:unit |
| Go | Go test | 80% | go test ./... |
| Python | pytest | 80% | pytest |
| Frontend | Jest | 70% components | npm 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 Type | Framework | Coverage Target |
|---|---|---|
| Node.js | Jest + Supertest | 100% happy path + key error paths |
| Go | Go test + httptest | 100% happy path + key error paths |
| Python | pytest + httpx | 100% 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
| Environment | Database | Gateway | Redis | Meilisearch | Purpose |
|---|---|---|---|---|---|
| Local | Local PostgreSQL (Docker) | MOCK | Local Redis | Local Meilisearch | Developer testing |
| CI (GitHub Actions) | PostgreSQL service container | MOCK | Redis service container | Meilisearch service container | Automated tests per PR |
| Staging | Staging PostgreSQL | PawaPay/Flutterwave sandbox | Staging Redis | Staging Meilisearch | Pre-release QA |
| Production | Production PostgreSQL | PawaPay/Flutterwave live | Production Redis | Production Meilisearch | Live 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 on4001, gateway test on4000) - Reset before each test run:
prisma migrate reset --force
4. Coverage Requirements
| Layer | Minimum 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 components | 70% |
| 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
productionrequire 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 auditrun weekly. - Nightly: Full regression test suite on staging.
6. Test Data Management
prisma/seed.tsprovides 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.,
0977000001for 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.