单元测试、集成测试、E2E测试详解 | 从零基础到精通 | 包含框架、工具和最佳实践
测试是保证代码质量的关键。没有测试的代码就像没有安全网的高空表演,一个小错误就可能导致严重后果。
// 安装Jest
npm install --save-dev jest
// package.json
{
"scripts": {
"test": "jest"
}
}
// 编写测试
describe('Calculator', () => {
test('should add two numbers', () => {
const result = add(2, 3);
expect(result).toBe(5);
});
test('should subtract two numbers', () => {
const result = subtract(5, 3);
expect(result).toBe(2);
});
});// 相等性
expect(value).toBe(expected); // 严格相等
expect(value).toEqual(expected); // 深度相等
// 真假值
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
// 数字
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThan(5);
expect(value).toBeCloseTo(0.3, 5);
// 字符串
expect(value).toMatch(/pattern/);
expect(value).toContain('substring');
// 数组
expect(array).toContain(item);
expect(array).toHaveLength(3);
// 异常
expect(() => {
throw new Error('test');
}).toThrow('test');// 使用async/await
test('should fetch data', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
// 使用done回调
test('should call callback', (done) => {
fetchData((error, data) => {
expect(error).toBeNull();
expect(data).toBeDefined();
done();
});
});
// 使用Promise
test('should return promise', () => {
return fetchData().then(data => {
expect(data).toBeDefined();
});
});describe('Database', () => {
beforeAll(() => {
// 在所有测试前执行一次
console.log('Connect to database');
});
afterAll(() => {
// 在所有测试后执行一次
console.log('Disconnect from database');
});
beforeEach(() => {
// 在每个测试前执行
console.log('Clear database');
});
afterEach(() => {
// 在每个测试后执行
console.log('Reset state');
});
test('should insert data', () => {
// 测试代码
});
});const request = require('supertest');
const app = require('../app');
describe('User API', () => {
test('POST /api/users should create a user', async () => {
const response = await request(app)
.post('/api/users')
.send({
username: 'alice',
email: 'alice@example.com',
password: 'password123'
})
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.username).toBe('alice');
});
test('GET /api/users/:id should return a user', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('username');
});
test('PUT /api/users/:id should update a user', async () => {
const response = await request(app)
.put('/api/users/1')
.send({ username: 'bob' })
.expect(200);
expect(response.body.username).toBe('bob');
});
test('DELETE /api/users/:id should delete a user', async () => {
await request(app)
.delete('/api/users/1')
.expect(204);
});
});describe('User Repository', () => {
let db;
beforeAll(async () => {
db = await connectToTestDatabase();
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
await db.clearDatabase();
});
test('should save and retrieve user', async () => {
const user = { username: 'alice', email: 'alice@example.com' };
const savedUser = await User.create(user);
const retrievedUser = await User.findById(savedUser.id);
expect(retrievedUser.username).toBe('alice');
});
test('should update user', async () => {
const user = await User.create({ username: 'alice' });
await User.updateById(user.id, { username: 'bob' });
const updated = await User.findById(user.id);
expect(updated.username).toBe('bob');
});
});// 安装Cypress
npm install --save-dev cypress
// cypress/e2e/login.cy.js
describe('Login Flow', () => {
beforeEach(() => {
cy.visit('http://localhost:3000/login');
});
test('should login successfully', () => {
cy.get('input[name="email"]').type('alice@example.com');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.get('h1').should('contain', 'Welcome');
});
test('should show error for invalid credentials', () => {
cy.get('input[name="email"]').type('wrong@example.com');
cy.get('input[name="password"]').type('wrongpassword');
cy.get('button[type="submit"]').click();
cy.get('.error-message').should('contain', 'Invalid credentials');
});
});// 导航
cy.visit('http://localhost:3000');
cy.go('back');
cy.reload();
// 选择元素
cy.get('selector');
cy.contains('text');
cy.find('selector');
// 交互
cy.click();
cy.type('text');
cy.select('option');
cy.check();
cy.uncheck();
// 断言
cy.get('h1').should('contain', 'Welcome');
cy.get('input').should('have.value', 'test');
cy.get('button').should('be.visible');
cy.get('button').should('be.disabled');
// 等待
cy.wait(1000);
cy.get('element').should('exist');// 安装Playwright
npm install --save-dev @playwright/test
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
test('should login successfully', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.fill('input[name="email"]', 'alice@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('h1')).toContainText('Welcome');
});// 第1步:编写失败的测试(Red)
describe('User Service', () => {
test('should validate email format', () => {
expect(validateEmail('invalid')).toBe(false);
expect(validateEmail('valid@example.com')).toBe(true);
});
});
// 第2步:编写最少代码使测试通过(Green)
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// 第3步:重构代码(Refactor)
function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}// package.json
{
"scripts": {
"test:coverage": "jest --coverage"
}
}
// 运行
npm run test:coverage
// 输出示例
File | % Stmts | % Branch | % Funcs | % Lines
--------------------|---------|----------|---------|--------
All files | 85.5 | 80.2 | 90.0 | 85.5
src/calculator.js | 100 | 100 | 100 | 100
src/utils.js | 70 | 60 | 80 | 70// 使用Jest的mock
const mockFetch = jest.fn();
test('should call API', async () => {
mockFetch.mockResolvedValue({ data: 'test' });
const result = await fetchData();
expect(mockFetch).toHaveBeenCalled();
expect(mockFetch).toHaveBeenCalledWith('/api/data');
expect(result).toEqual({ data: 'test' });
});
// 模拟模块
jest.mock('../api', () => ({
fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' })
}));// 使用Sinon库
const sinon = require('sinon');
test('should call callback', () => {
const callback = sinon.stub();
processData(callback);
expect(callback.calledOnce).toBe(true);
expect(callback.firstCall.args[0]).toBe('expected');
});
// 模拟时间
const clock = sinon.useFakeTimers();
setTimeout(() => {
console.log('After 1 second');
}, 1000);
clock.tick(1000); // 快进1秒
clock.restore();// 使用nock库
const nock = require('nock');
test('should fetch user data', async () => {
nock('http://api.example.com')
.get('/users/1')
.reply(200, { id: 1, name: 'Alice' });
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});// .github/workflows/test.yml
name: Tests
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: '16'
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
- name: Generate coverage
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v2project/
├── src/
│ ├── calculator.js
│ ├── userService.js
│ └── api.js
├── tests/
│ ├── unit/
│ │ ├── calculator.test.js
│ │ └── userService.test.js
│ ├── integration/
│ │ └── api.test.js
│ └── e2e/
│ └── login.cy.js
├── jest.config.js
└── package.json好的测试套件是高质量代码的基础。从单元测试开始,逐步添加集成和E2E测试。
现在你已经掌握了软件测试的核心知识。
测试不是负担,而是投资。好的测试能让你更快、更自信地开发。