testing-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Best Practices
测试最佳实践
This skill provides comprehensive expert knowledge of testing Node.js/Express applications with emphasis on Jest and Supertest, test organization, mocking strategies, and achieving comprehensive test coverage.
本技能提供Node.js/Express应用测试的全面专业知识,重点涵盖Jest与Supertest的使用、测试组织、模拟策略以及实现全面测试覆盖率的方法。
Testing Framework Setup
测试框架搭建
Jest Installation and Configuration
Jest安装与配置
Install dependencies:
bash
npm install --save-dev jest supertest @types/jestpackage.json configuration:
json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:verbose": "jest --verbose"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
],
"testMatch": [
"**/__tests__/**/*.js",
"**/?(*.)+(spec|test).js"
]
}
}jest.config.js (advanced):
javascript
module.exports = {
// Use Node.js test environment
testEnvironment: 'node',
// Test file patterns
testMatch: [
'**/__tests__/**/*.js',
'**/*.test.js',
'**/*.spec.js'
],
// Coverage settings
collectCoverageFrom: [
'src/**/*.js',
'routes/**/*.js',
'!src/index.js', // Exclude entry point
'!**/node_modules/**'
],
// Coverage thresholds
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
// Setup files
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
// Clear mocks between tests
clearMocks: true,
// Verbose output
verbose: true,
// Timeout for tests
testTimeout: 10000
};安装依赖:
bash
npm install --save-dev jest supertest @types/jestpackage.json配置:
json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:verbose": "jest --verbose"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
],
"testMatch": [
"**/__tests__/**/*.js",
"**/?(*.)+(spec|test).js"
]
}
}jest.config.js(进阶配置):
javascript
module.exports = {
// 使用Node.js测试环境
testEnvironment: 'node',
// 测试文件匹配规则
testMatch: [
'**/__tests__/**/*.js',
'**/*.test.js',
'**/*.spec.js'
],
// 覆盖率统计范围
collectCoverageFrom: [
'src/**/*.js',
'routes/**/*.js',
'!src/index.js', // 排除入口文件
'!**/node_modules/**'
],
// 覆盖率阈值
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
// 测试启动文件
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
// 测试间清除模拟
clearMocks: true,
// 详细输出
verbose: true,
// 测试超时时间
testTimeout: 10000
};Test Directory Structure
测试目录结构
Option 1: Separate test directory:
project/
├── src/
│ ├── server.js
│ ├── routes/
│ │ └── api.js
│ └── utils/
│ └── validators.js
├── test/
│ ├── setup.js
│ ├── server.test.js
│ ├── routes/
│ │ └── api.test.js
│ └── utils/
│ └── validators.test.js
└── package.jsonOption 2: Co-located tests:
project/
├── src/
│ ├── server.js
│ ├── server.test.js
│ ├── routes/
│ │ ├── api.js
│ │ └── api.test.js
│ └── utils/
│ ├── validators.js
│ └── validators.test.js
└── package.jsonOption 3: tests directories:
project/
├── src/
│ ├── __tests__/
│ │ └── server.test.js
│ ├── server.js
│ ├── routes/
│ │ ├── __tests__/
│ │ │ └── api.test.js
│ │ └── api.js
└── package.json方案1:独立测试目录:
project/
├── src/
│ ├── server.js
│ ├── routes/
│ │ └── api.js
│ └── utils/
│ └── validators.js
├── test/
│ ├── setup.js
│ ├── server.test.js
│ ├── routes/
│ │ └── api.test.js
│ └── utils/
│ └── validators.test.js
└── package.json方案2:测试文件与源码同目录:
project/
├── src/
│ ├── server.js
│ ├── server.test.js
│ ├── routes/
│ │ ├── api.js
│ │ └── api.test.js
│ └── utils/
│ ├── validators.js
│ └── validators.test.js
└── package.json方案3:__tests__目录:
project/
├── src/
│ ├── __tests__/
│ │ └── server.test.js
│ ├── server.js
│ ├── routes/
│ │ ├── __tests__/
│ │ │ └── api.test.js
│ │ └── api.js
└── package.jsonTesting Express Applications with Supertest
使用Supertest测试Express应用
Basic API Testing
基础API测试
javascript
const request = require('supertest');
const app = require('../server');
describe('GET /', () => {
it('should return 200 status', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
});
it('should return JSON content type', async () => {
const response = await request(app).get('/api/users');
expect(response.headers['content-type']).toMatch(/json/);
});
it('should return users array', async () => {
const response = await request(app).get('/api/users');
expect(response.body).toHaveProperty('users');
expect(Array.isArray(response.body.users)).toBe(true);
});
});
describe('POST /api/users', () => {
it('should create a user with valid data', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'SecurePass123!'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.set('Content-Type', 'application/json')
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.email).toBe(userData.email);
expect(response.body).not.toHaveProperty('password'); // Don't return password
});
it('should reject invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John',
email: 'invalid-email',
password: 'SecurePass123!'
})
.expect(400);
expect(response.body).toHaveProperty('error');
expect(response.body.error).toMatch(/email/i);
});
it('should reject weak password', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John',
email: 'john@example.com',
password: '123' // Too short
})
.expect(400);
expect(response.body).toHaveProperty('error');
expect(response.body.error).toMatch(/password/i);
});
});
describe('Authentication', () => {
let authToken;
beforeAll(async () => {
// Create a test user and get token
const response = await request(app)
.post('/api/login')
.send({
email: 'test@example.com',
password: 'TestPass123!'
});
authToken = response.body.token;
});
it('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body).toHaveProperty('user');
});
it('should reject access without token', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
it('should reject invalid token', async () => {
await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});javascript
const request = require('supertest');
const app = require('../server');
describe('GET /', () => {
it('should return 200 status', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
});
it('should return JSON content type', async () => {
const response = await request(app).get('/api/users');
expect(response.headers['content-type']).toMatch(/json/);
});
it('should return users array', async () => {
const response = await request(app).get('/api/users');
expect(response.body).toHaveProperty('users');
expect(Array.isArray(response.body.users)).toBe(true);
});
});
describe('POST /api/users', () => {
it('should create a user with valid data', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'SecurePass123!'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.set('Content-Type', 'application/json')
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.email).toBe(userData.email);
expect(response.body).not.toHaveProperty('password'); // 不返回密码
});
it('should reject invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John',
email: 'invalid-email',
password: 'SecurePass123!'
})
.expect(400);
expect(response.body).toHaveProperty('error');
expect(response.body.error).toMatch(/email/i);
});
it('should reject weak password', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John',
email: 'john@example.com',
password: '123' // 过短
})
.expect(400);
expect(response.body).toHaveProperty('error');
expect(response.body.error).toMatch(/password/i);
});
});
describe('Authentication', () => {
let authToken;
beforeAll(async () => {
// 创建测试用户并获取令牌
const response = await request(app)
.post('/api/login')
.send({
email: 'test@example.com',
password: 'TestPass123!'
});
authToken = response.body.token;
});
it('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body).toHaveProperty('user');
});
it('should reject access without token', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
it('should reject invalid token', async () => {
await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});Testing Proxy Endpoints
测试代理端点
javascript
const request = require('supertest');
const axios = require('axios');
const app = require('../server');
// Mock axios
jest.mock('axios');
describe('POST /api/proxy', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should proxy request successfully', async () => {
const mockData = {
results: [
{ id: 1, name: 'Result 1' },
{ id: 2, name: 'Result 2' }
]
};
axios.post.mockResolvedValue({
data: mockData,
status: 200
});
const response = await request(app)
.post('/api/proxy')
.send({ query: 'test' })
.expect(200);
expect(response.body).toEqual(mockData);
expect(axios.post).toHaveBeenCalledWith(
expect.any(String),
{ query: 'test' },
expect.any(Object)
);
});
it('should handle proxy errors', async () => {
axios.post.mockRejectedValue({
response: {
status: 500,
data: { error: 'Internal Server Error' }
}
});
const response = await request(app)
.post('/api/proxy')
.send({ query: 'test' })
.expect(500);
expect(response.body).toHaveProperty('error');
});
it('should handle network errors', async () => {
axios.post.mockRejectedValue(new Error('Network error'));
const response = await request(app)
.post('/api/proxy')
.send({ query: 'test' })
.expect(500);
expect(response.body).toHaveProperty('error');
});
it('should validate request before proxying', async () => {
const response = await request(app)
.post('/api/proxy')
.send({ invalid: 'data' })
.expect(400);
expect(response.body).toHaveProperty('error');
expect(axios.post).not.toHaveBeenCalled();
});
});javascript
const request = require('supertest');
const axios = require('axios');
const app = require('../server');
// 模拟axios
jest.mock('axios');
describe('POST /api/proxy', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should proxy request successfully', async () => {
const mockData = {
results: [
{ id: 1, name: 'Result 1' },
{ id: 2, name: 'Result 2' }
]
};
axios.post.mockResolvedValue({
data: mockData,
status: 200
});
const response = await request(app)
.post('/api/proxy')
.send({ query: 'test' })
.expect(200);
expect(response.body).toEqual(mockData);
expect(axios.post).toHaveBeenCalledWith(
expect.any(String),
{ query: 'test' },
expect.any(Object)
);
});
it('should handle proxy errors', async () => {
axios.post.mockRejectedValue({
response: {
status: 500,
data: { error: 'Internal Server Error' }
}
});
const response = await request(app)
.post('/api/proxy')
.send({ query: 'test' })
.expect(500);
expect(response.body).toHaveProperty('error');
});
it('should handle network errors', async () => {
axios.post.mockRejectedValue(new Error('Network error'));
const response = await request(app)
.post('/api/proxy')
.send({ query: 'test' })
.expect(500);
expect(response.body).toHaveProperty('error');
});
it('should validate request before proxying', async () => {
const response = await request(app)
.post('/api/proxy')
.send({ invalid: 'data' })
.expect(400);
expect(response.body).toHaveProperty('error');
expect(axios.post).not.toHaveBeenCalled();
});
});Mocking Strategies
模拟策略
Mocking External APIs
模拟外部API
Mock entire module:
javascript
jest.mock('axios');
const axios = require('axios');
describe('External API calls', () => {
it('should fetch data from external API', async () => {
const mockData = { data: 'test' };
axios.get.mockResolvedValue({ data: mockData });
const result = await fetchExternalData();
expect(result).toEqual(mockData);
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/data');
});
});Mock specific functions:
javascript
const userService = require('../services/user');
jest.spyOn(userService, 'findById').mockResolvedValue({
id: 1,
name: 'Test User'
});
describe('User routes', () => {
it('should get user by id', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body.name).toBe('Test User');
expect(userService.findById).toHaveBeenCalledWith('1');
});
});Manual mocks:
javascript
// __mocks__/axios.js
module.exports = {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} })),
put: jest.fn(() => Promise.resolve({ data: {} })),
delete: jest.fn(() => Promise.resolve({ data: {} }))
};模拟整个模块:
javascript
jest.mock('axios');
const axios = require('axios');
describe('External API calls', () => {
it('should fetch data from external API', async () => {
const mockData = { data: 'test' };
axios.get.mockResolvedValue({ data: mockData });
const result = await fetchExternalData();
expect(result).toEqual(mockData);
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/data');
});
});模拟特定函数:
javascript
const userService = require('../services/user');
jest.spyOn(userService, 'findById').mockResolvedValue({
id: 1,
name: 'Test User'
});
describe('User routes', () => {
it('should get user by id', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body.name).toBe('Test User');
expect(userService.findById).toHaveBeenCalledWith('1');
});
});手动模拟:
javascript
// __mocks__/axios.js
module.exports = {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} })),
put: jest.fn(() => Promise.resolve({ data: {} })),
delete: jest.fn(() => Promise.resolve({ data: {} }))
};Mocking Database
模拟数据库
javascript
// Mock database module
jest.mock('../db');
const db = require('../db');
describe('Database operations', () => {
beforeEach(() => {
db.query.mockClear();
});
it('should query users', async () => {
const mockUsers = [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
];
db.query.mockResolvedValue({ rows: mockUsers });
const users = await User.findAll();
expect(users).toEqual(mockUsers);
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
});
it('should handle database errors', async () => {
db.query.mockRejectedValue(new Error('Connection failed'));
await expect(User.findAll()).rejects.toThrow('Connection failed');
});
});javascript
// 模拟数据库模块
jest.mock('../db');
const db = require('../db');
describe('Database operations', () => {
beforeEach(() => {
db.query.mockClear();
});
it('should query users', async () => {
const mockUsers = [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
];
db.query.mockResolvedValue({ rows: mockUsers });
const users = await User.findAll();
expect(users).toEqual(mockUsers);
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
});
it('should handle database errors', async () => {
db.query.mockRejectedValue(new Error('Connection failed'));
await expect(User.findAll()).rejects.toThrow('Connection failed');
});
});Mocking Environment Variables
模拟环境变量
javascript
describe('Environment configuration', () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
});
afterAll(() => {
process.env = originalEnv;
});
it('should use default port when PORT not set', () => {
delete process.env.PORT;
const config = require('../config');
expect(config.port).toBe(3000);
});
it('should use PORT from environment', () => {
process.env.PORT = '8080';
const config = require('../config');
expect(config.port).toBe(8080);
});
});javascript
describe('Environment configuration', () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
});
afterAll(() => {
process.env = originalEnv;
});
it('should use default port when PORT not set', () => {
delete process.env.PORT;
const config = require('../config');
expect(config.port).toBe(3000);
});
it('should use PORT from environment', () => {
process.env.PORT = '8080';
const config = require('../config');
expect(config.port).toBe(8080);
});
});Unit vs Integration vs E2E Testing
单元测试vs集成测试vs端到端测试
Unit Tests
单元测试
What: Test individual functions/modules in isolation
Example:
javascript
// validators.js
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function isStrongPassword(password) {
return password.length >= 12 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password) &&
/[^A-Za-z0-9]/.test(password);
}
module.exports = { isValidEmail, isStrongPassword };
// validators.test.js
const { isValidEmail, isStrongPassword } = require('./validators');
describe('Email validation', () => {
it('should accept valid email', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
it('should reject email without @', () => {
expect(isValidEmail('testexample.com')).toBe(false);
});
it('should reject email without domain', () => {
expect(isValidEmail('test@')).toBe(false);
});
it('should reject email with spaces', () => {
expect(isValidEmail('test @example.com')).toBe(false);
});
});
describe('Password validation', () => {
it('should accept strong password', () => {
expect(isStrongPassword('MyP@ssw0rd123!')).toBe(true);
});
it('should reject short password', () => {
expect(isStrongPassword('Short1!')).toBe(false);
});
it('should reject password without uppercase', () => {
expect(isStrongPassword('myp@ssw0rd123!')).toBe(false);
});
it('should reject password without special char', () => {
expect(isStrongPassword('MyPassword123')).toBe(false);
});
});定义: 孤立测试单个函数/模块
示例:
javascript
// validators.js
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function isStrongPassword(password) {
return password.length >= 12 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password) &&
/[^A-Za-z0-9]/.test(password);
}
module.exports = { isValidEmail, isStrongPassword };
// validators.test.js
const { isValidEmail, isStrongPassword } = require('./validators');
describe('Email validation', () => {
it('should accept valid email', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
it('should reject email without @', () => {
expect(isValidEmail('testexample.com')).toBe(false);
});
it('should reject email without domain', () => {
expect(isValidEmail('test@')).toBe(false);
});
it('should reject email with spaces', () => {
expect(isValidEmail('test @example.com')).toBe(false);
});
});
describe('Password validation', () => {
it('should accept strong password', () => {
expect(isStrongPassword('MyP@ssw0rd123!')).toBe(true);
});
it('should reject short password', () => {
expect(isStrongPassword('Short1!')).toBe(false);
});
it('should reject password without uppercase', () => {
expect(isStrongPassword('myp@ssw0rd123!')).toBe(false);
});
it('should reject password without special char', () => {
expect(isStrongPassword('MyPassword123')).toBe(false);
});
});Integration Tests
集成测试
What: Test multiple components working together
Example:
javascript
const request = require('supertest');
const app = require('../server');
const db = require('../db');
describe('User registration flow', () => {
beforeEach(async () => {
// Clean database before each test
await db.query('DELETE FROM users');
});
it('should register user and allow login', async () => {
// Register user
const registerResponse = await request(app)
.post('/api/register')
.send({
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
})
.expect(201);
expect(registerResponse.body).toHaveProperty('id');
// Login with registered credentials
const loginResponse = await request(app)
.post('/api/login')
.send({
email: 'test@example.com',
password: 'SecurePass123!'
})
.expect(200);
expect(loginResponse.body).toHaveProperty('token');
// Access protected route with token
const profileResponse = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${loginResponse.body.token}`)
.expect(200);
expect(profileResponse.body.email).toBe('test@example.com');
});
});定义: 测试多个组件协同工作
示例:
javascript
const request = require('supertest');
const app = require('../server');
const db = require('../db');
describe('User registration flow', () => {
beforeEach(async () => {
// 每次测试前清理数据库
await db.query('DELETE FROM users');
});
it('should register user and allow login', async () => {
// 注册用户
const registerResponse = await request(app)
.post('/api/register')
.send({
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
})
.expect(201);
expect(registerResponse.body).toHaveProperty('id');
// 使用注册凭证登录
const loginResponse = await request(app)
.post('/api/login')
.send({
email: 'test@example.com',
password: 'SecurePass123!'
})
.expect(200);
expect(loginResponse.body).toHaveProperty('token');
// 使用令牌访问受保护路由
const profileResponse = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${loginResponse.body.token}`)
.expect(200);
expect(profileResponse.body.email).toBe('test@example.com');
});
});End-to-End (E2E) Tests
端到端(E2E)测试
What: Test complete user workflows from UI to database
Setup with Puppeteer:
bash
npm install --save-dev puppeteerExample:
javascript
const puppeteer = require('puppeteer');
describe('E2E: User registration', () => {
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
});
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
it('should complete registration flow', async () => {
// Navigate to registration page
await page.goto('http://localhost:3000/register');
// Fill out form
await page.type('#email', 'test@example.com');
await page.type('#password', 'SecurePass123!');
await page.type('#confirmPassword', 'SecurePass123!');
// Submit form
await page.click('button[type="submit"]');
// Wait for redirect to dashboard
await page.waitForNavigation();
// Verify we're on dashboard
const url = page.url();
expect(url).toContain('/dashboard');
// Verify welcome message
const welcomeMessage = await page.$eval(
'.welcome',
el => el.textContent
);
expect(welcomeMessage).toContain('test@example.com');
});
});定义: 测试从UI到数据库的完整用户工作流
使用Puppeteer搭建:
bash
npm install --save-dev puppeteer示例:
javascript
const puppeteer = require('puppeteer');
describe('E2E: User registration', () => {
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
});
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
it('should complete registration flow', async () => {
// 导航到注册页面
await page.goto('http://localhost:3000/register');
// 填写表单
await page.type('#email', 'test@example.com');
await page.type('#password', 'SecurePass123!');
await page.type('#confirmPassword', 'SecurePass123!');
// 提交表单
await page.click('button[type="submit"]');
// 等待重定向到仪表盘
await page.waitForNavigation();
// 验证是否在仪表盘页面
const url = page.url();
expect(url).toContain('/dashboard');
// 验证欢迎消息
const welcomeMessage = await page.$eval(
'.welcome',
el => el.textContent
);
expect(welcomeMessage).toContain('test@example.com');
});
});Test Organization
测试组织
Describe Blocks
Describe块
javascript
describe('User API', () => {
describe('GET /api/users', () => {
it('should return all users', async () => {
// Test implementation
});
it('should support pagination', async () => {
// Test implementation
});
it('should support filtering', async () => {
// Test implementation
});
});
describe('POST /api/users', () => {
it('should create user with valid data', async () => {
// Test implementation
});
it('should reject duplicate email', async () => {
// Test implementation
});
});
describe('PUT /api/users/:id', () => {
it('should update user', async () => {
// Test implementation
});
it('should reject unauthorized update', async () => {
// Test implementation
});
});
});javascript
describe('User API', () => {
describe('GET /api/users', () => {
it('should return all users', async () => {
// 测试实现
});
it('should support pagination', async () => {
// 测试实现
});
it('should support filtering', async () => {
// 测试实现
});
});
describe('POST /api/users', () => {
it('should create user with valid data', async () => {
// 测试实现
});
it('should reject duplicate email', async () => {
// 测试实现
});
});
describe('PUT /api/users/:id', () => {
it('should update user', async () => {
// 测试实现
});
it('should reject unauthorized update', async () => {
// 测试实现
});
});
});Setup and Teardown
前置与后置操作
javascript
describe('Database tests', () => {
// Runs once before all tests in this describe block
beforeAll(async () => {
await db.connect();
});
// Runs once after all tests in this describe block
afterAll(async () => {
await db.disconnect();
});
// Runs before each test in this describe block
beforeEach(async () => {
await db.query('DELETE FROM users');
await db.query('INSERT INTO users (email) VALUES ($1)', ['test@example.com']);
});
// Runs after each test in this describe block
afterEach(async () => {
jest.clearAllMocks();
});
it('should find user', async () => {
const user = await User.findByEmail('test@example.com');
expect(user).toBeTruthy();
});
it('should delete user', async () => {
await User.deleteByEmail('test@example.com');
const user = await User.findByEmail('test@example.com');
expect(user).toBeNull();
});
});javascript
describe('Database tests', () => {
// 在当前describe块的所有测试前运行一次
beforeAll(async () => {
await db.connect();
});
// 在当前describe块的所有测试后运行一次
afterAll(async () => {
await db.disconnect();
});
// 在当前describe块的每个测试前运行
beforeEach(async () => {
await db.query('DELETE FROM users');
await db.query('INSERT INTO users (email) VALUES ($1)', ['test@example.com']);
});
// 在当前describe块的每个测试后运行
afterEach(async () => {
jest.clearAllMocks();
});
it('should find user', async () => {
const user = await User.findByEmail('test@example.com');
expect(user).toBeTruthy();
});
it('should delete user', async () => {
await User.deleteByEmail('test@example.com');
const user = await User.findByEmail('test@example.com');
expect(user).toBeNull();
});
});Test Fixtures
测试夹具
javascript
// test/fixtures/users.js
module.exports = {
validUser: {
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
},
adminUser: {
email: 'admin@example.com',
password: 'AdminPass123!',
name: 'Admin User',
role: 'admin'
},
invalidUsers: {
noEmail: {
password: 'SecurePass123!',
name: 'Test User'
},
weakPassword: {
email: 'test@example.com',
password: '123',
name: 'Test User'
}
}
};
// Usage in tests
const fixtures = require('./fixtures/users');
describe('User creation', () => {
it('should create valid user', async () => {
const response = await request(app)
.post('/api/users')
.send(fixtures.validUser)
.expect(201);
});
it('should reject user without email', async () => {
const response = await request(app)
.post('/api/users')
.send(fixtures.invalidUsers.noEmail)
.expect(400);
});
});javascript
// test/fixtures/users.js
module.exports = {
validUser: {
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
},
adminUser: {
email: 'admin@example.com',
password: 'AdminPass123!',
name: 'Admin User',
role: 'admin'
},
invalidUsers: {
noEmail: {
password: 'SecurePass123!',
name: 'Test User'
},
weakPassword: {
email: 'test@example.com',
password: '123',
name: 'Test User'
}
}
};
// 在测试中使用
const fixtures = require('./fixtures/users');
describe('User creation', () => {
it('should create valid user', async () => {
const response = await request(app)
.post('/api/users')
.send(fixtures.validUser)
.expect(201);
});
it('should reject user without email', async () => {
const response = await request(app)
.post('/api/users')
.send(fixtures.invalidUsers.noEmail)
.expect(400);
});
});Async Testing
异步测试
Testing Promises
测试Promise
javascript
describe('Async operations', () => {
it('should resolve with data', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
it('should reject with error', async () => {
await expect(fetchInvalidData()).rejects.toThrow('Not found');
});
// Alternative: using done callback
it('should fetch data (callback style)', (done) => {
fetchData()
.then(data => {
expect(data).toBeDefined();
done();
})
.catch(done);
});
});javascript
describe('Async operations', () => {
it('should resolve with data', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
it('should reject with error', async () => {
await expect(fetchInvalidData()).rejects.toThrow('Not found');
});
// 替代方案:使用done回调
it('should fetch data (callback style)', (done) => {
fetchData()
.then(data => {
expect(data).toBeDefined();
done();
})
.catch(done);
});
});Testing Callbacks
测试回调函数
javascript
describe('Callback functions', () => {
it('should call callback with data', (done) => {
fetchDataWithCallback((err, data) => {
expect(err).toBeNull();
expect(data).toBeDefined();
done();
});
});
it('should call callback with error', (done) => {
fetchInvalidDataWithCallback((err, data) => {
expect(err).toBeTruthy();
expect(data).toBeUndefined();
done();
});
});
});javascript
describe('Callback functions', () => {
it('should call callback with data', (done) => {
fetchDataWithCallback((err, data) => {
expect(err).toBeNull();
expect(data).toBeDefined();
done();
});
});
it('should call callback with error', (done) => {
fetchInvalidDataWithCallback((err, data) => {
expect(err).toBeTruthy();
expect(data).toBeUndefined();
done();
});
});
});Code Coverage
代码覆盖率
Generating Coverage Reports
生成覆盖率报告
bash
undefinedbash
undefinedRun tests with coverage
带覆盖率运行测试
npm run test:coverage
npm run test:coverage
Coverage report output
覆盖率报告输出
----------|---------|----------|---------|---------|
| File | % Stmts | % Branch | % Funcs | % Lines |
|---|---|---|---|---|
| All files | 85.5 | 78.3 | 91.2 | 85.1 |
| server.js | 92.3 | 85.7 | 100 | 91.8 |
| routes/ | 78.9 | 71.4 | 83.3 | 79.2 |
| ---------- | --------- | ---------- | --------- | --------- |
undefined----------|---------|----------|---------|---------|
| File | % Stmts | % Branch | % Funcs | % Lines |
|---|---|---|---|---|
| All files | 85.5 | 78.3 | 91.2 | 85.1 |
| server.js | 92.3 | 85.7 | 100 | 91.8 |
| routes/ | 78.9 | 71.4 | 83.3 | 79.2 |
| ---------- | --------- | ---------- | --------- | --------- |
undefinedCoverage Configuration
覆盖率配置
javascript
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.js',
'!src/index.js', // Exclude entry point
'!src/**/*.test.js', // Exclude test files
'!src/**/__tests__/**' // Exclude test directories
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
// Per-file thresholds
'./src/critical-module.js': {
branches: 100,
functions: 100,
lines: 100,
statements: 100
}
},
coverageReporters: [
'text', // Terminal output
'html', // HTML report in coverage/
'lcov', // For CI tools
'json' // Machine-readable
]
};javascript
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.js',
'!src/index.js', // 排除入口文件
'!src/**/*.test.js', // 排除测试文件
'!src/**/__tests__/**' // 排除测试目录
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
// 按文件设置阈值
'./src/critical-module.js': {
branches: 100,
functions: 100,
lines: 100,
statements: 100
}
},
coverageReporters: [
'text', // 终端输出
'html', // 生成HTML报告在coverage/目录
'lcov', // 用于CI工具
'json' // 机器可读格式
]
};Viewing HTML Coverage Report
查看HTML覆盖率报告
bash
npm run test:coverage
open coverage/index.html # macOS
xdg-open coverage/index.html # Linux
start coverage/index.html # Windowsbash
npm run test:coverage
open coverage/index.html # macOS
xdg-open coverage/index.html # Linux
start coverage/index.html # WindowsTesting Best Practices
测试最佳实践
1. Naming Conventions
1. 命名规范
javascript
// GOOD - Descriptive test names
describe('User registration', () => {
it('should create user with valid email and password', () => {});
it('should reject registration with duplicate email', () => {});
it('should hash password before storing', () => {});
});
// BAD - Vague test names
describe('User', () => {
it('works', () => {});
it('test 1', () => {});
it('should not fail', () => {});
});javascript
// 良好 - 描述性测试名称
describe('User registration', () => {
it('should create user with valid email and password', () => {});
it('should reject registration with duplicate email', () => {});
it('should hash password before storing', () => {});
});
// 糟糕 - 模糊的测试名称
describe('User', () => {
it('works', () => {});
it('test 1', () => {});
it('should not fail', () => {});
});2. AAA Pattern (Arrange, Act, Assert)
2. AAA模式(准备、执行、断言)
javascript
it('should calculate total price with tax', () => {
// Arrange: Set up test data
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 }
];
const taxRate = 0.1;
// Act: Perform the action
const total = calculateTotal(items, taxRate);
// Assert: Verify the result
expect(total).toBe(38.5); // (10*2 + 5*3) * 1.1
});javascript
it('should calculate total price with tax', () => {
// Arrange: 设置测试数据
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 }
];
const taxRate = 0.1;
// Act: 执行操作
const total = calculateTotal(items, taxRate);
// Assert: 验证结果
expect(total).toBe(38.5); // (10*2 + 5*3) * 1.1
});3. Test One Thing
3. 单一测试原则
javascript
// GOOD - Each test checks one behavior
it('should validate email format', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
it('should reject email without domain', () => {
expect(isValidEmail('test@')).toBe(false);
});
// BAD - Testing multiple things
it('should validate inputs', () => {
expect(isValidEmail('test@example.com')).toBe(true);
expect(isValidPassword('pass123')).toBe(false);
expect(isValidPhone('1234567890')).toBe(true);
});javascript
// 良好 - 每个测试检查一个行为
it('should validate email format', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
it('should reject email without domain', () => {
expect(isValidEmail('test@')).toBe(false);
});
// 糟糕 - 测试多个内容
it('should validate inputs', () => {
expect(isValidEmail('test@example.com')).toBe(true);
expect(isValidPassword('pass123')).toBe(false);
expect(isValidPhone('1234567890')).toBe(true);
});4. Avoid Test Interdependence
4. 避免测试依赖
javascript
// BAD - Tests depend on each other
let userId;
it('should create user', async () => {
const response = await createUser();
userId = response.id; // Other tests depend on this
});
it('should update user', async () => {
await updateUser(userId); // Fails if previous test fails
});
// GOOD - Each test is independent
describe('User operations', () => {
let userId;
beforeEach(async () => {
const user = await createUser();
userId = user.id;
});
it('should update user', async () => {
await updateUser(userId);
});
it('should delete user', async () => {
await deleteUser(userId);
});
});javascript
// 糟糕 - 测试相互依赖
let userId;
it('should create user', async () => {
const response = await createUser();
userId = response.id; // 其他测试依赖此值
});
it('should update user', async () => {
await updateUser(userId); // 如果前一个测试失败,此测试也会失败
});
// 良好 - 每个测试独立
describe('User operations', () => {
let userId;
beforeEach(async () => {
const user = await createUser();
userId = user.id;
});
it('should update user', async () => {
await updateUser(userId);
});
it('should delete user', async () => {
await deleteUser(userId);
});
});5. Use Meaningful Assertions
5. 使用有意义的断言
javascript
// GOOD - Specific assertions
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('users');
expect(response.body.users).toHaveLength(5);
expect(response.body.users[0]).toMatchObject({
id: expect.any(Number),
email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)
});
// BAD - Vague assertions
expect(response).toBeTruthy();
expect(response.body).toBeDefined();javascript
// 良好 - 具体的断言
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('users');
expect(response.body.users).toHaveLength(5);
expect(response.body.users[0]).toMatchObject({
id: expect.any(Number),
email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)
});
// 糟糕 - 模糊的断言
expect(response).toBeTruthy();
expect(response.body).toBeDefined();6. Test Edge Cases
6. 测试边缘情况
javascript
describe('Division function', () => {
it('should divide positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('should handle negative numbers', () => {
expect(divide(-10, 2)).toBe(-5);
});
it('should handle zero numerator', () => {
expect(divide(0, 5)).toBe(0);
});
it('should throw error for division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
it('should handle decimal results', () => {
expect(divide(5, 2)).toBe(2.5);
});
});javascript
describe('Division function', () => {
it('should divide positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('should handle negative numbers', () => {
expect(divide(-10, 2)).toBe(-5);
});
it('should handle zero numerator', () => {
expect(divide(0, 5)).toBe(0);
});
it('should throw error for division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
it('should handle decimal results', () => {
expect(divide(5, 2)).toBe(2.5);
});
});CI/CD Integration
CI/CD集成
GitHub Actions
GitHub Actions
yaml
undefinedyaml
undefined.github/workflows/test.yml
.github/workflows/test.yml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Generate coverage report
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/coverage-final.json
fail_ci_if_error: trueundefinedname: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Generate coverage report
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/coverage-final.json
fail_ci_if_error: trueundefinednpm Scripts for CI
用于CI的npm脚本
json
{
"scripts": {
"test": "jest",
"test:ci": "jest --ci --coverage --maxWorkers=2",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch"
}
}json
{
"scripts": {
"test": "jest",
"test:ci": "jest --ci --coverage --maxWorkers=2",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch"
}
}Common Jest Matchers
常用Jest匹配器
Equality
相等性
javascript
expect(value).toBe(4); // Strict equality (===)
expect(value).toEqual({ a: 1 }); // Deep equality
expect(value).not.toBe(5); // Negationjavascript
expect(value).toBe(4); // 严格相等 (===)
expect(value).toEqual({ a: 1 }); // 深度相等
expect(value).not.toBe(5); // 取反Truthiness
真值判断
javascript
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();javascript
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();Numbers
数字
javascript
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
expect(value).toBeCloseTo(0.3); // Floating pointjavascript
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
expect(value).toBeCloseTo(0.3); // 浮点数比较Strings
字符串
javascript
expect(string).toMatch(/pattern/);
expect(string).toMatch('substring');
expect(string).toContain('substring');javascript
expect(string).toMatch(/pattern/);
expect(string).toMatch('substring');
expect(string).toContain('substring');Arrays and Iterables
数组与可迭代对象
javascript
expect(array).toContain('item');
expect(array).toHaveLength(3);
expect(array).toEqual(expect.arrayContaining([1, 2]));javascript
expect(array).toContain('item');
expect(array).toHaveLength(3);
expect(array).toEqual(expect.arrayContaining([1, 2]));Objects
对象
javascript
expect(object).toHaveProperty('key');
expect(object).toHaveProperty('key', value);
expect(object).toMatchObject({ a: 1, b: 2 });
expect(object).toEqual(expect.objectContaining({ a: 1 }));javascript
expect(object).toHaveProperty('key');
expect(object).toHaveProperty('key', value);
expect(object).toMatchObject({ a: 1, b: 2 });
expect(object).toEqual(expect.objectContaining({ a: 1 }));Functions
函数
javascript
expect(fn).toThrow();
expect(fn).toThrow('error message');
expect(fn).toThrow(Error);
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(3);javascript
expect(fn).toThrow();
expect(fn).toThrow('error message');
expect(fn).toThrow(Error);
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(3);Testing Checklist
测试检查清单
Unit Tests
单元测试
- Test pure functions in isolation
- Test all code paths (happy path and error cases)
- Test edge cases and boundary conditions
- Mock external dependencies
- Achieve high code coverage (>80%)
- 孤立测试纯函数
- 测试所有代码路径(正常流程和错误场景)
- 测试边缘情况和边界条件
- 模拟外部依赖
- 实现高代码覆盖率(>80%)
Integration Tests
集成测试
- Test API endpoints
- Test authentication/authorization
- Test database operations
- Test external API integration
- Test error handling
- 测试API端点
- 测试认证/授权
- 测试数据库操作
- 测试外部API集成
- 测试错误处理
E2E Tests
端到端测试
- Test critical user flows
- Test form submissions
- Test navigation
- Test authentication flow
- 测试关键用户流程
- 测试表单提交
- 测试导航
- 测试认证流程
General
通用
- Tests are fast (< 5 seconds for unit tests)
- Tests are independent (can run in any order)
- Tests are repeatable (same result every time)
- Tests have clear, descriptive names
- Setup and teardown properly implemented
- No hardcoded values (use constants/fixtures)
- CI/CD integration configured
- 测试速度快(单元测试<5秒)
- 测试独立(可按任意顺序运行)
- 测试可重复(每次结果一致)
- 测试名称清晰、具有描述性
- 正确实现前置与后置操作
- 无硬编码值(使用常量/夹具)
- 配置CI/CD集成
Example Test Suite for Express API
Express API测试套件示例
javascript
const request = require('supertest');
const app = require('../server');
const db = require('../db');
describe('Express API Tests', () => {
// Setup: Connect to test database
beforeAll(async () => {
await db.connect(process.env.TEST_DATABASE_URL);
});
// Cleanup: Disconnect from database
afterAll(async () => {
await db.disconnect();
});
// Reset database before each test
beforeEach(async () => {
await db.query('DELETE FROM users');
});
describe('GET /api/health', () => {
it('should return health status', async () => {
const response = await request(app)
.get('/api/health')
.expect(200);
expect(response.body).toEqual({
status: 'ok',
timestamp: expect.any(Number)
});
});
});
describe('POST /api/users', () => {
it('should create user with valid data', async () => {
const userData = {
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(Number),
email: userData.email,
name: userData.name
});
expect(response.body).not.toHaveProperty('password');
});
it('should reject duplicate email', async () => {
const userData = {
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
};
// Create first user
await request(app).post('/api/users').send(userData);
// Try to create duplicate
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(409);
expect(response.body.error).toMatch(/already exists/i);
});
it('should validate email format', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
password: 'SecurePass123!',
name: 'Test'
})
.expect(400);
expect(response.body.error).toMatch(/email/i);
});
it('should enforce password requirements', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
password: 'weak',
name: 'Test'
})
.expect(400);
expect(response.body.error).toMatch(/password/i);
});
});
describe('Authentication', () => {
let authToken;
const testUser = {
email: 'auth@example.com',
password: 'SecurePass123!',
name: 'Auth User'
};
beforeEach(async () => {
// Create user
await request(app).post('/api/users').send(testUser);
// Login and get token
const response = await request(app)
.post('/api/login')
.send({
email: testUser.email,
password: testUser.password
});
authToken = response.body.token;
});
it('should login with valid credentials', async () => {
const response = await request(app)
.post('/api/login')
.send({
email: testUser.email,
password: testUser.password
})
.expect(200);
expect(response.body).toHaveProperty('token');
});
it('should reject invalid credentials', async () => {
await request(app)
.post('/api/login')
.send({
email: testUser.email,
password: 'WrongPassword'
})
.expect(401);
});
it('should access protected route with token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body.email).toBe(testUser.email);
});
it('should reject access without token', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
});
});javascript
const request = require('supertest');
const app = require('../server');
const db = require('../db');
describe('Express API Tests', () => {
// 前置:连接测试数据库
beforeAll(async () => {
await db.connect(process.env.TEST_DATABASE_URL);
});
// 清理:断开数据库连接
afterAll(async () => {
await db.disconnect();
});
// 每次测试前重置数据库
beforeEach(async () => {
await db.query('DELETE FROM users');
});
describe('GET /api/health', () => {
it('should return health status', async () => {
const response = await request(app)
.get('/api/health')
.expect(200);
expect(response.body).toEqual({
status: 'ok',
timestamp: expect.any(Number)
});
});
});
describe('POST /api/users', () => {
it('should create user with valid data', async () => {
const userData = {
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(Number),
email: userData.email,
name: userData.name
});
expect(response.body).not.toHaveProperty('password');
});
it('should reject duplicate email', async () => {
const userData = {
email: 'test@example.com',
password: 'SecurePass123!',
name: 'Test User'
};
// 创建第一个用户
await request(app).post('/api/users').send(userData);
// 尝试创建重复用户
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(409);
expect(response.body.error).toMatch(/already exists/i);
});
it('should validate email format', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
password: 'SecurePass123!',
name: 'Test'
})
.expect(400);
expect(response.body.error).toMatch(/email/i);
});
it('should enforce password requirements', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
password: 'weak',
name: 'Test'
})
.expect(400);
expect(response.body.error).toMatch(/password/i);
});
});
describe('Authentication', () => {
let authToken;
const testUser = {
email: 'auth@example.com',
password: 'SecurePass123!',
name: 'Auth User'
};
beforeEach(async () => {
// 创建用户
await request(app).post('/api/users').send(testUser);
// 登录并获取令牌
const response = await request(app)
.post('/api/login')
.send({
email: testUser.email,
password: testUser.password
});
authToken = response.body.token;
});
it('should login with valid credentials', async () => {
const response = await request(app)
.post('/api/login')
.send({
email: testUser.email,
password: testUser.password
})
.expect(200);
expect(response.body).toHaveProperty('token');
});
it('should reject invalid credentials', async () => {
await request(app)
.post('/api/login')
.send({
email: testUser.email,
password: 'WrongPassword'
})
.expect(401);
});
it('should access protected route with token', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body.email).toBe(testUser.email);
});
it('should reject access without token', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
});
});Resources
资源
- Jest Documentation: https://jestjs.io/docs/getting-started
- Supertest Documentation: https://github.com/ladjs/supertest
- Testing Best Practices: https://github.com/goldbergyoni/javascript-testing-best-practices
- Kent C. Dodds Testing Library: https://testing-library.com/
- Node.js Testing Best Practices: https://github.com/goldbergyoni/nodebestpractices#6-testing-best-practices
- Jest文档: https://jestjs.io/docs/getting-started
- Supertest文档: https://github.com/ladjs/supertest
- 测试最佳实践: https://github.com/goldbergyoni/javascript-testing-best-practices
- Kent C. Dodds测试库: https://testing-library.com/
- Node.js测试最佳实践: https://github.com/goldbergyoni/nodebestpractices#6-testing-best-practices