Files
pos-system/docs/en/skills/testing-patterns.md
Ho Ngoc Hai b104fafa85 Refactor auth-service to iam-service and update related documentation
- Renamed auth-service to iam-service across various files for consistency.
- Updated Dockerfiles, deployment configurations, and documentation to reflect the service name change.
- Enhanced testing commands in documentation to point to the new iam-service.
- Removed outdated auth-service files and configurations to streamline the project structure.
- Improved bilingual documentation for clarity on the new service structure and usage.
2025-12-30 20:54:21 +07:00

739 lines
22 KiB
Markdown

# Testing Patterns
> **EN**: Comprehensive testing best practices for GoodGo microservices including unit tests, integration tests, E2E tests, Jest configuration, mocking strategies, and debugging techniques.
> **VI**: Các thực hành tốt nhất về testing cho microservices GoodGo bao gồm unit tests, integration tests, E2E tests, cấu hình Jest, strategies mocking, và kỹ thuật debugging.
## Overview / Tổng Quan
**EN**: Testing is a critical component of the GoodGo microservices platform. This guide provides comprehensive patterns and best practices for writing maintainable, reliable tests across all services. It covers test types, Jest configuration, mocking strategies, test utilities, and debugging techniques that ensure code quality and reliability.
**VI**: Testing là thành phần quan trọng của nền tảng microservices GoodGo. Hướng dẫn này cung cấp các patterns và best practices toàn diện để viết các test có thể bảo trì và đáng tin cậy trên tất cả các services. Nó bao gồm các loại test, cấu hình Jest, strategies mocking, test utilities, và kỹ thuật debugging để đảm bảo chất lượng và độ tin cậy của code.
## When to Use / Khi Nào Sử Dụng
**EN**: Use these testing patterns when:
- Writing unit tests for services, controllers, or repositories
- Creating integration tests for middleware chains
- Building E2E tests for API endpoints
- Setting up Jest configuration for a new service
- Mocking external dependencies (Prisma, Redis, Auth SDK)
- Debugging test failures
- Improving test coverage
- Creating test utilities and factories
**VI**: Sử dụng các testing patterns này khi:
- Viết unit tests cho services, controllers, hoặc repositories
- Tạo integration tests cho middleware chains
- Xây dựng E2E tests cho API endpoints
- Thiết lập cấu hình Jest cho service mới
- Mocking các dependencies bên ngoài (Prisma, Redis, Auth SDK)
- Debugging test failures
- Cải thiện test coverage
- Tạo test utilities và factories
## Key Concepts / Khái Niệm Chính
### Test Types / Các Loại Test
#### 1. Unit Tests / Unit Tests
**EN**: Test individual functions or classes in isolation with all dependencies mocked.
**VI**: Test các functions hoặc classes riêng lẻ một cách cô lập với tất cả dependencies được mock.
**Characteristics**:
- **Location**: Next to source files (`*.test.ts`) or in `__tests__/` directory
- **Scope**: Single function, method, or class
- **Dependencies**: All external dependencies mocked
- **Speed**: Fast (<1s per test)
- **Purpose**: Verify logic correctness
**Example**: [`services/iam-service/src/modules/feature/__tests__/feature.service.test.ts`](../../../services/iam-service/src/modules/feature/__tests__/feature.service.test.ts)
#### 2. Integration Tests / Integration Tests
**EN**: Test multiple components working together with some real dependencies.
**VI**: Test nhiều components làm việc cùng nhau với một số dependencies thật.
**Characteristics**:
- **Location**: `__tests__/` directory (`*.test.ts`)
- **Scope**: Multiple components interacting
- **Dependencies**: Mix of real and mocked dependencies
- **Speed**: Medium (1-5s per test)
- **Purpose**: Verify component integration
**Example**: [`services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts`](../../../services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts)
#### 3. E2E Tests / E2E Tests
**EN**: Test complete request/response cycles through the entire application stack.
**VI**: Test các chu kỳ request/response hoàn chỉnh qua toàn bộ application stack.
**Characteristics**:
- **Location**: `__tests__/*.e2e.ts`
- **Scope**: Full API workflow from HTTP request to response
- **Dependencies**: Test database, mocked external services
- **Speed**: Slow (5-10s per test)
- **Purpose**: Verify end-to-end functionality
**Example**: [`services/iam-service/src/__tests__/feature.e2e.ts`](../../../services/iam-service/src/__tests__/feature.e2e.ts)
## Jest Configuration / Cấu Hình Jest
### Standard Configuration
The standard Jest configuration for GoodGo services includes TypeScript support, coverage thresholds, and proper test environment setup.
**Location**: [`services/iam-service/jest.config.ts`](../../../services/iam-service/jest.config.ts)
```typescript
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: [
'**/__tests__/**/*.test.ts',
'**/__tests__/**/*.spec.ts',
'**/__tests__/**/*.e2e.ts',
'**/?(*.)+(spec|test).ts'
],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/main.ts',
'!src/config/**/*.ts',
'!src/**/*.config.ts'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
},
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setupTests.ts'],
testTimeout: 10000,
clearMocks: true,
resetModules: true
};
export default config;
```
### Key Configuration Points
- **preset: 'ts-jest'**: Enables TypeScript support
- **testEnvironment: 'node'**: Sets Node.js environment (not browser)
- **coverageThreshold**: Enforces minimum 70% coverage across all metrics
- **setupFilesAfterEnv**: Loads test setup file before each test suite
- **clearMocks**: Automatically clears mock calls between tests
- **resetModules**: Resets module cache for test isolation
## Setup Files / Files Thiết Lập
### Test Setup File
The setup file (`setupTests.ts`) configures global mocks and test utilities that are available across all tests.
**Location**: [`services/iam-service/src/__tests__/setupTests.ts`](../../../services/iam-service/src/__tests__/setupTests.ts)
**Key Features**:
- Mock external services (logger, tracing, database, Redis)
- Configure test environment variables
- Set up global test utilities
- Initialize Prometheus mocks
**Example**:
```typescript
import { jest } from '@jest/globals';
// EN: Mock environment variables for tests
// VI: Mock biến môi trường cho tests
process.env.NODE_ENV = 'test';
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test_db';
process.env.REDIS_URL = 'redis://localhost:6379/1';
// EN: Mock external services to avoid real network calls
// VI: Mock các service bên ngoài để tránh gọi mạng thật
jest.mock('@goodgo/logger', () => ({
logger: {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
},
}));
// EN: Global test utilities
// VI: Utilities test toàn cục
global.testUtils = {
createMockReq: (overrides = {}) => ({
body: {},
params: {},
query: {},
headers: {},
...overrides,
}),
createMockRes: () => {
const res: any = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
res.send = jest.fn().mockReturnValue(res);
return res;
},
createMockNext: () => jest.fn(),
};
```
## Common Patterns / Các Pattern Thường Dùng
### Unit Test Pattern
**Structure**: Follow the AAA (Arrange-Act-Assert) pattern for clarity.
```typescript
describe('FeatureService', () => {
let service: FeatureService;
let mockRepository: any;
beforeEach(() => {
// EN: Clear all mocks before each test
// VI: Xóa tất cả mocks trước mỗi test
jest.clearAllMocks();
mockRepository = {
findById: jest.fn(),
create: jest.fn(),
};
service = new FeatureService(mockRepository);
});
describe('create', () => {
it('should create a feature successfully', async () => {
// EN: Arrange
// VI: Chuẩn bị
const testData = { name: 'test-feature', title: 'Test Feature' };
const mockFeature = {
id: 'test-id',
name: testData.name,
// ... other fields
};
mockRepository.create.mockResolvedValue(mockFeature);
// EN: Act
// VI: Thực hiện
const result = await service.create(testData);
// EN: Assert
// VI: Kiểm tra
expect(mockRepository.create).toHaveBeenCalledWith(testData);
expect(result).toEqual(mockFeature);
});
});
});
```
**Real Example**: [`services/iam-service/src/modules/feature/__tests__/feature.service.test.ts`](../../../services/iam-service/src/modules/feature/__tests__/feature.service.test.ts)
### Integration Test Pattern
Integration tests verify that multiple components work together correctly.
```typescript
describe('Auth Middleware', () => {
const mockNext = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('should authenticate valid token and attach user to request', () => {
// EN: Arrange
// VI: Chuẩn bị
const mockReq = createMockReq({
headers: { authorization: 'Bearer valid-token' },
});
const mockRes = createMockRes();
// EN: Act
// VI: Thực hiện
const middleware = authenticate({ secret: jwtSecret });
middleware(mockReq as Request, mockRes as Response, mockNext);
// EN: Assert
// VI: Kiểm tra
expect(mockNext).toHaveBeenCalled();
expect(mockReq.user).toBeDefined();
});
});
```
**Real Example**: [`services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts`](../../../services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts)
### E2E Test Pattern
E2E tests use supertest to test complete HTTP request/response cycles.
```typescript
import request from 'supertest';
import express from 'express';
import { createRouter } from '../routes';
describe('Feature Endpoints E2E', () => {
let app: express.Application;
beforeAll(() => {
// EN: Set up test environment
// VI: Thiết lập môi trường test
process.env.NODE_ENV = 'test';
app = express();
app.use(express.json());
app.use(createRouter());
});
describe('POST /api/v1/features', () => {
it('should create a feature successfully', async () => {
// EN: Arrange
// VI: Chuẩn bị
const featureData = {
name: 'test-feature',
title: 'Test Feature',
description: 'A test feature for E2E testing'
};
// EN: Act
// VI: Thực hiện
const response = await request(app)
.post('/api/v1/features')
.send(featureData)
.expect(201);
// EN: Assert
// VI: Kiểm tra
expect(response.body).toMatchObject({
success: true,
message: 'Feature created successfully / Feature đã được tạo thành công',
});
});
});
});
```
**Real Example**: [`services/iam-service/src/__tests__/feature.e2e.ts`](../../../services/iam-service/src/__tests__/feature.e2e.ts)
## Mocking Strategies / Chiến Lược Mocking
### Mock Prisma Database
Mock Prisma client to avoid real database connections in unit tests.
```typescript
// In setupTests.ts or individual test files
jest.mock('../config/database.config', () => ({
connectDatabase: jest.fn(),
prisma: {
$queryRaw: jest.fn(),
$disconnect: jest.fn(),
feature: {
create: jest.fn(),
findMany: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
},
}));
// Usage in tests
const { prisma } = require('../config/database.config');
test('should create user', async () => {
prisma.feature.create.mockResolvedValue({
id: 'test-id',
name: 'test-feature',
// ... other fields
});
const result = await createFeature({ name: 'test-feature' });
expect(result).toBeDefined();
});
```
### Mock Redis
Mock Redis client for caching and session management tests.
```typescript
jest.mock('../config/redis.config', () => ({
getRedisClient: jest.fn().mockReturnValue({
call: jest.fn(),
connect: jest.fn(),
disconnect: jest.fn(),
}),
}));
```
### Mock External APIs
Mock external HTTP calls using Jest mocks.
```typescript
jest.mock('axios');
import axios from 'axios';
const mockedAxios = axios as jest.Mocked<typeof axios>;
test('should fetch external data', async () => {
mockedAxios.get.mockResolvedValue({
data: { result: 'success' }
});
const result = await fetchExternalData();
expect(result).toEqual({ result: 'success' });
});
```
### Mock Auth SDK
Mock authentication SDK functions for middleware and service tests.
```typescript
jest.mock('@goodgo/auth-sdk', () => ({
createToken: jest.fn(),
verifyToken: jest.fn(),
extractTokenFromHeader: jest.fn(),
}));
// Setup mock implementations
(verifyToken as jest.Mock).mockImplementation((token) => {
if (token === 'valid-token') {
return { userId: '123', role: 'user' };
}
throw new Error('Invalid token');
});
```
## Testing Utilities / Utilities Test
### Test Factory Pattern
Create reusable test data factories for consistent test data.
```typescript
export class TestFactory {
static createUser(overrides = {}) {
return {
id: 'test-user-1',
email: 'test@example.com',
name: 'Test User',
createdAt: new Date(),
...overrides
};
}
static createAuthToken(userId: string) {
return jwt.sign({ userId }, 'test-secret');
}
static async cleanDatabase() {
await prisma.user.deleteMany();
await prisma.feature.deleteMany();
}
}
// Usage
const user = TestFactory.createUser({ name: 'Custom Name' });
const token = TestFactory.createAuthToken(user.id);
```
### Mock Request/Response Helpers
Use global test utilities for creating mock Express request/response objects.
```typescript
// Available via global.testUtils (from setupTests.ts)
const mockReq = global.testUtils.createMockReq({
headers: { authorization: 'Bearer token' },
params: { id: '123' },
});
const mockRes = global.testUtils.createMockRes();
const mockNext = global.testUtils.createMockNext();
```
## Common Test Scenarios / Các Tình Huống Test Thường Gặp
### Testing Error Handling
```typescript
it('should handle database errors gracefully', async () => {
// EN: Arrange - Mock database error
// VI: Chuẩn bị - Mock lỗi database
prismaMock.user.findUnique.mockRejectedValue(
new Error('Database connection failed')
);
// EN: Act & Assert
// VI: Thực hiện & Kiểm tra
await expect(userService.findById('123')).rejects.toThrow();
// Or for HTTP endpoints
const response = await request(app)
.get('/api/users/123')
.expect(500);
expect(response.body).toEqual({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'Internal server error / Lỗi máy chủ nội bộ',
},
});
});
```
### Testing Validation
```typescript
describe('Validation', () => {
it('should reject invalid email', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({ email: 'invalid-email', password: '123456' })
.expect(400);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
});
```
### Testing Authorization
```typescript
it('should deny access for user with incorrect role', () => {
const mockReq = createMockReq({
user: { userId: 'user-123', role: 'user' },
});
const middleware = authorize('admin');
middleware(mockReq as Request, mockRes as Response, mockNext);
expect(mockNext).not.toHaveBeenCalled();
expect(mockStatus).toHaveBeenCalledWith(403);
});
```
## Test Commands / Lệnh Test
Add these scripts to `package.json`:
```json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest --testPathPattern=\\.test\\.ts$",
"test:e2e": "jest --testPathPattern=\\.e2e\\.ts$",
"test:ci": "jest --coverage --silent --maxWorkers=2"
}
}
```
**Usage**:
- `pnpm test`: Run all tests
- `pnpm test:watch`: Run tests in watch mode
- `pnpm test:coverage`: Generate coverage report
- `pnpm test:unit`: Run only unit tests
- `pnpm test:e2e`: Run only E2E tests
- `pnpm test:ci`: Run tests optimized for CI/CD
## Debugging Tests / Debugging Tests
### VS Code Debug Configuration
Create `.vscode/launch.json` for debugging tests:
```json
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Jest Tests",
"runtimeExecutable": "npm",
"runtimeArgs": ["test", "--", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
```
### Debug Tips
1. **Use `test.only()`** to run a single test:
```typescript
it.only('should test specific behavior', () => {
// Only this test will run
});
```
2. **Use `--detectOpenHandles`** for async issues:
```bash
pnpm test --detectOpenHandles
```
3. **Use `--runInBand`** for sequential execution:
```bash
pnpm test --runInBand
```
4. **Add temporary console.log**:
```typescript
console.log('Debug value:', someValue);
```
5. **Use debugger breakpoints** in VS Code
## Best Practices / Thực Hành Tốt Nhất
### Test Organization
- ✅ Each test is independent and isolated
- ✅ Tests follow AAA pattern (Arrange-Act-Assert)
- ✅ Use descriptive test names that explain what is being tested
- ✅ Group related tests using `describe` blocks
- ✅ Use `beforeEach`/`afterEach` for setup/cleanup
### Mocking
- ✅ Mock external dependencies (database, APIs, services)
- ✅ Use `jest.clearAllMocks()` in `beforeEach` to reset mocks
- ✅ Verify mock calls to ensure correct behavior
- ✅ Keep mocks simple and focused
### Coverage
- ✅ Maintain >70% code coverage (as per Jest config)
- ✅ Focus on covering critical business logic
- ✅ Don't sacrifice test quality for coverage percentage
- ✅ Review coverage reports regularly
### Test Data
- ✅ Use factories for creating test data
- ✅ Keep test data realistic and representative
- ✅ Clean up test data after tests (if using real database)
- ✅ Use meaningful test values, not just `'test'` or `123`
### Error Testing
- ✅ Test both success and error scenarios
- ✅ Test edge cases and boundary conditions
- ✅ Test validation errors
- ✅ Test error messages and error codes
### Performance
- ✅ Keep unit tests fast (<1s each)
- ✅ Avoid unnecessary async operations in unit tests
- ✅ Use `--maxWorkers` in CI for parallel execution
- ✅ Don't test implementation details, test behavior
## Examples from Project / Ví Dụ Từ Dự Án
### Real Test Examples
1. **Unit Test**: [`services/iam-service/src/modules/feature/__tests__/feature.service.test.ts`](../../../services/iam-service/src/modules/feature/__tests__/feature.service.test.ts)
2. **Integration Test**: [`services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts`](../../../services/iam-service/src/middlewares/__tests__/auth.middleware.test.ts)
3. **E2E Test**: [`services/iam-service/src/__tests__/feature.e2e.ts`](../../../services/iam-service/src/__tests__/feature.e2e.ts)
4. **Setup File**: [`services/iam-service/src/__tests__/setupTests.ts`](../../../services/iam-service/src/__tests__/setupTests.ts)
5. **Jest Config**: [`services/iam-service/jest.config.ts`](../../../services/iam-service/jest.config.ts)
### Test Structure Examples
- **Repository Tests**: [`services/iam-service/src/modules/feature/__tests__/feature.repository.test.ts`](../../../services/iam-service/src/modules/feature/__tests__/feature.repository.test.ts)
- **Controller Tests**: [`services/iam-service/src/modules/health/__tests__/health.controller.test.ts`](../../../services/iam-service/src/modules/health/__tests__/health.controller.test.ts)
- **Middleware Tests**: [`services/iam-service/src/middlewares/__tests__/correlation.middleware.test.ts`](../../../services/iam-service/src/middlewares/__tests__/correlation.middleware.test.ts)
## Quick Reference / Tham Khảo Nhanh
### Test Type Decision Tree
```
Need to test complete HTTP flow?
├─ Yes → E2E Test (*.e2e.ts)
└─ No → Multiple components interacting?
├─ Yes → Integration Test (*.test.ts)
└─ No → Unit Test (*.test.ts)
```
### Common Jest Matchers
| Matcher | Purpose | Example |
|---------|---------|---------|
| `toBe()` | Exact equality | `expect(value).toBe(5)` |
| `toEqual()` | Deep equality | `expect(obj).toEqual({a: 1})` |
| `toMatchObject()` | Partial match | `expect(obj).toMatchObject({a: 1})` |
| `toHaveBeenCalled()` | Function called | `expect(mockFn).toHaveBeenCalled()` |
| `toHaveBeenCalledWith()` | Called with args | `expect(mockFn).toHaveBeenCalledWith('arg')` |
| `toThrow()` | Throws error | `expect(() => fn()).toThrow()` |
| `toBeDefined()` | Not undefined | `expect(value).toBeDefined()` |
| `toBeNull()` | Is null | `expect(value).toBeNull()` |
| `toContain()` | Array/string contains | `expect(array).toContain('item')` |
### Mock Function Helpers
```typescript
// Create mock
const mockFn = jest.fn();
// Set return value
mockFn.mockReturnValue('value');
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('error'));
// Set implementation
mockFn.mockImplementation((arg) => arg * 2);
// Clear/reset
mockFn.mockClear(); // Clear call history
mockFn.mockReset(); // Reset to initial state
jest.clearAllMocks(); // Clear all mocks
```
## Related Skills / Skills Liên Quan
- **[Comment Code](./comment-code.md)**: Writing bilingual comments in tests
- **[Security](./security.md)**: Testing security-critical code
- **[API Design](./api-design.md)**: Testing API endpoints and responses
- **[Project Rules](./project-rules.md)**: Code organization for tests
## Resources / Tài Nguyên
### Official Documentation
- [Jest Documentation](https://jestjs.io/docs/getting-started)
- [Supertest Documentation](https://github.com/visionmedia/supertest)
- [jest-mock-extended](https://github.com/marchaos/jest-mock-extended)
### Internal Documentation
- [Service Development Guide](../guides/development.md)
- [Local Development Setup](../guides/local-development.md)
- [Troubleshooting Guide](../guides/troubleshooting.md)
### Tools
- **Jest**: JavaScript testing framework
- **Supertest**: HTTP assertion library
- **jest-mock-extended**: Enhanced mocking for TypeScript
- **ioredis-mock**: Redis mock for testing