软件测试基础知识总结
写代码是创造,测试是不把炸弹留给半年后的自己。 一个人的项目,往往是上线之后才有空补测试——反过来才对。
一、什么是软件测试
软件测试是验证软件是否符合预期的过程。本质上做两件事:
- 找 bug:代码行为和设计不符的地方
- 防回归:今天修好的 bug,下次改代码不能又冒出来
1.1 测试 ≠ 运行一下
很多人说"我测试过了",其实就是在浏览器里点了几下。这叫手动冒烟,有用但远远不够。真正的测试要能自动跑、反复跑、在改了代码之后马上告诉你有没有坏。
1.2 质量金字塔
业界有个共识叫测试金字塔:
╱ E2E ╲ ← 端到端:模拟真实用户,数量最少 ╱─────────╲ ╱ Integration ╲ ← 集成:模块间协作,中间层 ╱───────────────╲ ╱ Unit Test ╲ ← 单元:测一个函数,数量最多、跑得最快 ╱───────────────────╲| 层级 | 测什么 | 谁写 | 跑一次 | 数量 |
|---|---|---|---|---|
| 单元测试 | 一个函数/方法的输出 | 开发 | 毫秒 | 几百到几千 |
| 集成测试 | 数据库、缓存、第三方 API | 开发 | 秒级 | 几十到几百 |
| 端到端测试 | 完整业务流程(登录→下单→支付) | 开发或测试 | 分钟 | 个位数 |
底层越多越稳。 如果把金字塔倒过来——只测 E2E 不测单元——就是一个脆弱的测试体系。
二、测试分类一览表
| 分类 | 关注点 | 示例 |
|---|---|---|
| 功能测试 | 能不能用 | 登录、注册、下单、支付 |
| 回归测试 | 改了代码有没有把旧功能搞坏 | 每次发布前跑一遍全量用例 |
| 性能测试 | 扛不扛得住 | 1000 并发下接口 P95 响应时间 |
| 安全测试 | 有没有漏洞 | SQL 注入、XSS、越权访问 |
| 兼容性测试 | 在不同环境下表现是否一致 | Chrome / Safari / iOS App / 微信小程序 |
| 可用性测试 | 人用起来顺不顺 | 按钮够不够大、操作路径深不深 |
| 冒烟测试 | 新版本能不能基本跑起来 | 部署后先跑一遍核心流程 |
| A/B 测试 | 两个版本哪个效果好 | 新支付页面 vs 老页面,看转化率 |
三、单元测试怎么写
3.1 主流框架
| 语言 | 框架 |
|---|---|
| JavaScript / Node.js | Vitest(最快,推荐)、Jest |
| Python | PyTest |
| PHP | PHPUnit |
| Java | JUnit 5 |
| Go | 原生 testing 包(无框架) |
3.2 一个完整的单元测试(Vitest)
被测函数:一个计算订单总价的函数,有折扣、有税费。
// 订单计算函数(被测代码)export function calculateTotal(items, opts = {}) { const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0); const discount = opts.discount ?? 0; const discountAmount = subtotal * discount; const afterDiscount = subtotal - discountAmount; const taxRate = opts.taxRate ?? 0; const tax = afterDiscount * taxRate; return Math.round((afterDiscount + tax) * 100) / 100;}测试用例:
import { describe, it, expect } from 'vitest';import { calculateTotal } from './order.js';describe('calculateTotal', () => { // 正常情况 it('两件商品无折扣无税费', () => { const items = [ { price: 100, qty: 2 }, { price: 50, qty: 1 }, ]; expect(calculateTotal(items)).toBe(250); }); // 边界:空购物车 it('空购物车返回 0', () => { expect(calculateTotal([])).toBe(0); }); // 折扣 it('8 折折扣生效', () => { const items = [{ price: 100, qty: 1 }]; expect(calculateTotal(items, { discount: 0.2 })).toBe(80); }); // 折扣 + 税费 it('8 折 + 10% 税费', () => { const items = [{ price: 100, qty: 1 }]; // 100 × 0.8 × 1.1 = 88 expect(calculateTotal(items, { discount: 0.2, taxRate: 0.1 })).toBe(88); }); // 精度:保留两位小数 it('金额保留两位小数', () => { const items = [{ price: 33.33, qty: 3 }]; // 99.99 expect(calculateTotal(items)).toBe(99.99); });});3.3 好测试长什么样
一个好的单元测试应该:
- 一个 it 只测一件事——出了问题一眼就知道哪里坏了
- 起名说人话——
it('空购物车返回 0')比it('test empty case')清楚 100 倍 - 不依赖外部——不连数据库、不读文件、不调第三方 API
- 跑一遍毫秒级——慢了就没人愿意跑
四、集成测试
单元测试搞定了"一个函数能不能算出正确结果",但真实业务中函数要连数据库、调 Redis、发 HTTP 请求。集成测试就是测几个模块拼在一起时能不能正常协作。
4.1 典型场景
- API 测试:POST
/api/orders→ 返回 201 → 数据库真的插进去了一条 - 缓存测试:第一次查走数据库,第二次查走 Redis
- 消息队列:生产者发了消息 → 消费者收到了 → 业务动作执行了
4.2 怎么处理外部依赖
| 方式 | 说明 |
|---|---|
| 真实容器 | 用 Docker 起一个真实 MySQL 跑测试——最靠谱,但慢 |
| 内存数据库 | 比如 SQLite 替代 MySQL 做集成测试——快,但覆盖不到真实驱动问题 |
| Mock | 模拟外部依赖的返回值——速度最快,但测不到真实协作 |
一个务实的策略:单元测试全 Mock,集成测试用 Docker 起真实容器。
五、端到端测试(E2E)
E2E 测试模拟真实用户在浏览器里的操作:打开页面 → 点按钮 → 填表单 → 提交 → 验证结果。
5.1 为什么需要 E2E
单元测试和集成测试都过了,也可能用户一打开网站就白屏——可能是打包没成功、路由配错、环境变量漏了。E2E 是最后一道防线。
5.2 Playwright 速查
Playwright 是目前最流行的 E2E 框架,一个 API 驱动 Chromium、Firefox、WebKit。
import { test, expect } from '@playwright/test';test('完整下单流程', async ({ page }) => { // 打开首页 await page.goto('https://example.com'); // 搜索商品 await page.fill('input[placeholder="搜索"]', '耳机'); await page.click('button:has-text("搜索")'); // 点第一个结果 await page.click('.product-item:first-child'); // 加入购物车 await page.click('button:has-text("加入购物车")'); // 去结算 await page.click('a:has-text("购物车")'); await page.click('button:has-text("去结算")'); // 应该跳转到登录页或订单确认页 await expect(page).toHaveURL(/login|checkout/);});5.3 E2E 什么时候跑
别每次改一行代码就跑 E2E(慢)。推荐两个时机:
- PR 合并前——CI 里自动跑一遍 E2E
- 发布前——手动触发一次完整 E2E
六、AI 时代的测试
AI 写代码又快又多,但它自己不保证对。以下几个趋势值得关注:
6.1 AI 帮写测试
给 AI 一段被测代码,让它生成单元测试用例是可行的。常见做法:
- 先让 AI 列边界条件,人工确认后再让它写测试
- 让 AI 写测试 + 注入 bug——故意让一个测试失败,确认测试真的能抓到问题
6.2 AI 写的代码需要更多测试
AI 生成的代码有一个特点:逻辑看起来对,但边界条件经常错(空数组、负数、null、超长字符串……)。用 AI 生成的代码上线之前,人工 review + 单元测试覆盖边界条件是必须的。
6.3 测试不再是事后工作
一个值得养成的习惯:让 AI 先生成测试用例,再生成实现。这变相实现了 TDD。
七、常见坑
坑 1:测试写得比代码还长
一条原则:如果 mock 超过 5 个,说明被测模块设计有问题,该拆了。好的代码本身是好测的。
坑 2:CI 里不跑测试
本地跑绿了就 push,然后上线出事。测试只有跑在 CI 里才是真正的测试。一个最小的 GitHub Actions 配置:
name: Teston: [push, pull_request]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npm ci - run: npm test坑 3:测试里塞业务逻辑
测试应该描述意图,而不是重现实现。如果改一行代码需要同时改三个测试,说明测试绑死了实现细节,该重写了。
坑 4:覆盖率迷信
100% 覆盖率听起来厉害,但成本很高。一个务实的目标:
- 核心业务逻辑:80%+
- 工具函数 / 纯逻辑:90%+
- UI 层、胶水代码:不强制
八、一个人的实践方案
独立开发者没测试团队,但可以花一天时间搭好这套流程:
┌──────────────┐ 开发代码 ────────▶│ 本地 npm test│──── 红 → 改代码 → 绿 └──────────────┘ │ ▼ push ┌──────────────┐ │ CI 自动跑测试 │─── 红 → 拒绝合并,push 通知你 └──────────────┘ │ 绿 ▼ 合并 ┌──────────────┐ │ 自动部署上线 │ └──────────────┘ │ ▼ ┌──────────────┐ │ 跑 E2E 冒烟测 │─── 红 → 自动回滚 └──────────────┘这套机制一旦搭好,花在测试上的时间会被它救回来的事故次数百倍偿还。
九、常用框架对照表
| 语言 | 单元测试 | E2E | 接口测试工具 |
|---|---|---|---|
| JS / TS | Vitest、Jest | Playwright | Postman / Bruno |
| Python | PyTest | Playwright | Httpie |
| PHP | PHPUnit | Playwright | Insomnia |
| Java | JUnit 5 | Playwright | Apifox |
| Go | 原生 testing | — | curl |
十、记住这三句话
- 测试不是给 QA 写的,是给半年后的自己写的。
- 一个人开发,也值得有 CI。
- 如果你从没被"上次改的代码搞坏了这次的功能"坑过,那你还没写够代码。