Testing Strategy & Framework
Testing Pyramid
Test Categories
Unit Tests
// Example Unit Test
describe('PaymentService', () => {
let service: PaymentService;
let mockRepository: MockType<PaymentRepository>;
beforeEach(() => {
mockRepository = {
save: jest.fn(),
findById: jest.fn()
};
service = new PaymentService(mockRepository);
});
describe('processPayment', () => {
it('should process valid payment', async () => {
const payment = {
amount: 100,
currency: 'SAR',
userId: '123'
};
mockRepository.save.mockResolvedValue({ ...payment, id: '456' });
const result = await service.processPayment(payment);
expect(result.id).toBe('456');
expect(mockRepository.save).toHaveBeenCalledWith(payment);
});
it('should throw error for invalid amount', async () => {
const payment = {
amount: -100,
currency: 'SAR',
userId: '123'
};
await expect(service.processPayment(payment))
.rejects
.toThrow('Invalid payment amount');
});
});
});
Integration Tests
// Example Integration Test
describe('Payment Integration', () => {
let app: INestApplication;
let dbConnection: Connection;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
dbConnection = moduleRef.get<Connection>(Connection);
});
afterAll(async () => {
await dbConnection.close();
await app.close();
});
describe('POST /payments', () => {
it('should create payment', async () => {
const response = await request(app.getHttpServer())
.post('/payments')
.send({
amount: 100,
currency: 'SAR',
userId: '123'
});
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
// Verify database state
const payment = await dbConnection
.getRepository(Payment)
.findOne(response.body.id);
expect(payment).toBeDefined();
});
});
});
E2E Tests
// Example E2E Test
describe('Payment Flow', () => {
let page: Page;
beforeAll(async () => {
page = await browser.newPage();
await page.goto('https://staging.oanfinance.com');
});
afterAll(async () => {
await page.close();
});
it('should complete payment process', async () => {
// Login
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password');
await page.click('#login-button');
// Navigate to payment
await page.click('#make-payment');
// Fill payment details
await page.fill('#amount', '100');
await page.selectOption('#currency', 'SAR');
// Submit payment
await page.click('#submit-payment');
// Verify success
await expect(page.locator('.success-message'))
.toHaveText('Payment Successful');
});
});
Test Coverage Requirements
Coverage Thresholds
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
statements: 80,
branches: 80,
functions: 80,
lines: 80
},
'./src/core/': {
statements: 90,
branches: 90,
functions: 90,
lines: 90
}
}
};
Test Environment Setup
Docker Compose for Testing
version: '3.8'
services:
test-db:
image: postgres:13
environment:
POSTGRES_DB: oan_test
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
ports:
- "5432:5432"
test-redis:
image: redis:6
ports:
- "6379:6379"
test-mocks:
image: wiremock/wiremock
ports:
- "8080:8080"
volumes:
- ./mocks:/home/wiremock
Mocking Strategies
API Mocking
// Mock External API
const mockRiyadBankAPI = {
processPayment: jest.fn().mockResolvedValue({
transactionId: '123',
status: 'success'
}),
getStatus: jest.fn().mockResolvedValue('completed')
};
// Use in Tests
test('payment processing', async () => {
const result = await paymentService.process({
amount: 100,
currency: 'SAR'
});
expect(mockRiyadBankAPI.processPayment).toHaveBeenCalled();
expect(result.status).toBe('success');
});
Test Data Management
Data Factories
// User Factory
export const createUser = Factory.define<User>(() => ({
id: faker.datatype.uuid(),
name: faker.name.fullName(),
email: faker.internet.email(),
createdAt: faker.date.recent()
}));
// Payment Factory
export const createPayment = Factory.define<Payment>(() => ({
id: faker.datatype.uuid(),
amount: faker.datatype.number({ min: 100, max: 1000 }),
currency: 'SAR',
status: faker.helpers.arrayElement(['pending', 'completed', 'failed']),
createdAt: faker.date.recent()
}));
CI Integration
GitHub Actions Configuration
name: Test Pipeline
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install Dependencies
run: npm ci
- name: Run Linting
run: npm run lint
- name: Run Unit Tests
run: npm run test:unit
- name: Run Integration Tests
run: npm run test:integration
- name: Run E2E Tests
run: npm run test:e2e
- name: Upload Coverage
uses: actions/upload-artifact@v2
with:
name: coverage
path: coverage/
Performance Testing
Load Testing with k6
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Ramp up
{ duration: '3m', target: 50 }, // Stay at 50 users
{ duration: '1m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% requests within 500ms
http_req_failed: ['rate<0.01'], // Less than 1% failures
},
};
export default function () {
const response = http.get('https://api.oanfinance.com/health');
check(response, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
Security Testing
OWASP ZAP Integration
name: Security Scan
on:
schedule:
- cron: '0 0 * * *'
jobs:
zap_scan:
runs-on: ubuntu-latest
steps:
- name: ZAP Scan
uses: zaproxy/action-full-scan@v0.4.0
with:
target: 'https://staging.oanfinance.com'
Best Practices
Testing Guidelines
- Write tests first (TDD)
- Keep tests simple and focused
- Use meaningful test descriptions
- Maintain test independence
- Clean up test data
Code Quality
- Regular test maintenance
- Consistent naming conventions
- Proper error handling
- Clear test documentation
- Efficient test execution
Review Process
- Test coverage review
- Code quality check
- Performance impact
- Security implications
- Documentation updates