- Updated skill documentation files to include structured metadata for better organization. - Enhanced bilingual descriptions and guidelines for clarity in both English and Vietnamese. - Refined sections on usage, best practices, and related skills to ensure consistency across all documentation. - Improved formatting and removed outdated references to streamline the documentation experience. - Added best practices checklists to relevant skills for better usability and adherence to standards.
11 KiB
11 KiB
name, description
| name | description |
|---|---|
| testing-patterns | Testing best practices for GoodGo microservices. Use when writing unit tests, integration tests, E2E tests, setting up Jest, mocking dependencies, or debugging test failures. |
Testing Patterns for GoodGo Microservices
When to Use This Skill
Use this skill 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
Core Concepts
Test Types
-
Unit Tests: Test individual functions/classes in isolation
- Location: Next to source files (
*.test.ts) - Scope: Single function or class
- Dependencies: Mocked
- Speed: Fast (<1s per test)
- Location: Next to source files (
-
Integration Tests: Test component interactions
- Location:
__tests__/directory - Scope: Multiple components working together
- Dependencies: Some real, some mocked
- Speed: Medium (1-5s per test)
- Location:
-
E2E Tests: Test complete request/response cycles
- Location:
__tests__/*.e2e.ts - Scope: Full API workflow
- Dependencies: Test database, mocked external services
- Speed: Slow (5-10s per test)
- Location:
Jest Configuration
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: [
'**/__tests__/**/*.test.ts',
'**/__tests__/**/*.e2e.ts',
'**/?(*.)+(spec|test).ts'
],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/main.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
};
export default config;
Setup Files
// src/__tests__/setupTests.ts
import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaClient } from '@prisma/client';
// Mock Prisma
jest.mock('../prisma', () => ({
__esModule: true,
default: mockDeep<PrismaClient>()
}));
// Mock Redis
jest.mock('ioredis', () => {
const Redis = jest.requireActual('ioredis-mock');
return Redis;
});
// Global test utilities
global.testUtils = {
generateId: () => `test-${Date.now()}`,
createMockRequest: () => ({
headers: {},
body: {},
query: {},
params: {}
}),
createMockResponse: () => {
const res: any = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
res.send = jest.fn().mockReturnValue(res);
return res;
}
};
beforeEach(() => {
jest.clearAllMocks();
});
Testing Patterns
Unit Test Pattern
// feature.service.test.ts
import { FeatureService } from './feature.service';
import { mockDeep } from 'jest-mock-extended';
describe('FeatureService', () => {
let service: FeatureService;
let mockRepository: any;
beforeEach(() => {
mockRepository = {
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn()
};
service = new FeatureService(mockRepository);
});
describe('findById', () => {
it('should return feature when found', async () => {
// Arrange
const mockFeature = { id: '1', name: 'Test Feature' };
mockRepository.findById.mockResolvedValue(mockFeature);
// Act
const result = await service.findById('1');
// Assert
expect(result).toEqual(mockFeature);
expect(mockRepository.findById).toHaveBeenCalledWith('1');
});
it('should throw error when feature not found', async () => {
// Arrange
mockRepository.findById.mockResolvedValue(null);
// Act & Assert
await expect(service.findById('999')).rejects.toThrow('Feature not found');
});
});
});
Integration Test Pattern
// auth.middleware.test.ts
import { authMiddleware } from '../auth.middleware';
import { createMockRequest, createMockResponse } from '../../test-utils';
import jwt from 'jsonwebtoken';
describe('Auth Middleware', () => {
const mockNext = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('should call next when valid token provided', async () => {
// Arrange
const req = createMockRequest();
const res = createMockResponse();
const token = jwt.sign({ userId: '123' }, 'secret');
req.headers.authorization = `Bearer ${token}`;
// Act
await authMiddleware(req, res, mockNext);
// Assert
expect(mockNext).toHaveBeenCalled();
expect(req.user).toEqual({ userId: '123' });
});
it('should return 401 when no token provided', async () => {
// Arrange
const req = createMockRequest();
const res = createMockResponse();
// Act
await authMiddleware(req, res, mockNext);
// Assert
expect(res.status).toHaveBeenCalledWith(401);
expect(mockNext).not.toHaveBeenCalled();
});
});
E2E Test Pattern
// feature.e2e.ts
import supertest from 'supertest';
import { createApp } from '../app';
import { prisma } from '../prisma';
describe('Feature API E2E', () => {
let app: any;
let request: any;
beforeAll(async () => {
app = await createApp();
request = supertest(app);
});
afterAll(async () => {
await prisma.$disconnect();
});
beforeEach(async () => {
await prisma.feature.deleteMany();
});
describe('POST /api/features', () => {
it('should create a new feature', async () => {
// Arrange
const featureData = {
name: 'New Feature',
description: 'Feature description'
};
// Act
const response = await request
.post('/api/features')
.set('Authorization', 'Bearer valid-token')
.send(featureData)
.expect(201);
// Assert
expect(response.body).toMatchObject({
success: true,
data: {
name: 'New Feature',
description: 'Feature description'
}
});
const created = await prisma.feature.findFirst({
where: { name: 'New Feature' }
});
expect(created).toBeDefined();
});
});
});
Mocking Strategies
Mock Prisma
// __mocks__/prisma.ts
import { PrismaClient } from '@prisma/client';
import { mockDeep, DeepMockProxy } from 'jest-mock-extended';
export const prismaMock = mockDeep<PrismaClient>();
jest.mock('../src/prisma', () => ({
__esModule: true,
default: prismaMock,
}));
// Usage in tests
import { prismaMock } from '../__mocks__/prisma';
test('should create user', async () => {
const user = { id: '1', email: 'test@example.com' };
prismaMock.user.create.mockResolvedValue(user);
const result = await createUser({ email: 'test@example.com' });
expect(result).toEqual(user);
});
Mock Redis
// __mocks__/redis.ts
import Redis from 'ioredis-mock';
export const redisMock = new Redis();
// Usage in tests
test('should cache value', async () => {
const cache = new CacheService(redisMock);
await cache.set('key', 'value');
const result = await cache.get('key');
expect(result).toBe('value');
});
Mock External APIs
// Mock axios
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' });
});
Testing Utilities
// test-utils.ts
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);
Common Test Scenarios
Testing Error Handling
test('should handle database errors gracefully', async () => {
prismaMock.user.findUnique.mockRejectedValue(
new Error('Database connection failed')
);
const response = await request
.get('/api/users/123')
.expect(500);
expect(response.body).toEqual({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'Internal server error'
}
});
});
Testing Validation
describe('Validation', () => {
it('should reject invalid email', async () => {
const response = await request
.post('/api/auth/register')
.send({ email: 'invalid-email', password: '123456' })
.expect(400);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
});
Testing Pagination
test('should paginate results', async () => {
// Create test data
const items = Array(25).fill(null).map((_, i) => ({
id: `item-${i}`,
name: `Item ${i}`
}));
prismaMock.item.findMany.mockResolvedValue(items.slice(0, 10));
prismaMock.item.count.mockResolvedValue(25);
const response = await request
.get('/api/items?page=1&limit=10')
.expect(200);
expect(response.body).toEqual({
success: true,
data: items.slice(0, 10),
pagination: {
page: 1,
limit: 10,
total: 25,
totalPages: 3
}
});
});
Test Commands
// package.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"
}
}
Debugging Tests
Debug with VS Code
// .vscode/launch.json
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Jest Tests",
"runtimeExecutable": "npm",
"runtimeArgs": ["test", "--", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
Debug Tips
- Use
test.only()to run single test - Use
--detectOpenHandlesfor async issues - Use
--runInBandfor sequential execution - Add
console.log()statements temporarily - Use debugger breakpoints in VS Code
Best Practices Checklist
- Each test is independent and isolated
- Tests follow AAA pattern (Arrange-Act-Assert)
- Mock external dependencies
- Test edge cases and error scenarios
- Keep tests simple and focused
- Use descriptive test names
- Maintain >70% code coverage
- Run tests before committing
- Keep test data realistic
- Clean up after tests