📚 完整教程目录

前言:为什么需要测试

🤔 测试的重要性

测试是保证代码质量的关键。没有测试的代码就像没有安全网的高空表演,一个小错误就可能导致严重后果。

测试的收益

测试金字塔

💡 重点:测试不是可选的,是生产环境的必需品。

测试类型

按范围分类

按目的分类

按执行方式分类

单元测试

Jest框架

// 安装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(); }); });

Setup和Teardown

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', () => { // 测试代码 }); });

集成测试

测试API端点

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'); }); });

E2E测试

Cypress框架

// 安装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(现代替代品)

// 安装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'); });

测试驱动开发(TDD)

TDD流程

TDD示例

// 第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); }

TDD的好处

代码覆盖率

覆盖率指标

生成覆盖率报告

// 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

覆盖率目标

⚠️ 注意:100%覆盖率不等于没有bug,关键是测试的质量。

Mock和Stub

Mock对象

// 使用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' }) }));

Stub函数

// 使用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();

Mock HTTP请求

// 使用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'); });

CI/CD集成

GitHub Actions

// .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@v2

自动化测试流程

实战项目:完整的测试套件

项目结构

project/ ├── 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测试。

🎉 测试学习完成

现在你已经掌握了软件测试的核心知识。

✅ 你现在可以:

🚀 下一步学习

  1. 学习性能测试
  2. 学习安全测试
  3. 学习可视化回归测试
  4. 学习负载测试
💡 建议:

测试不是负担,而是投资。好的测试能让你更快、更自信地开发。