全栈手账 订阅 RSS

软件测试基础知识总结

写代码是创造,测试是不把炸弹留给半年后的自己。 一个人的项目,往往是上线之后才有空补测试——反过来才对。

读了 12 分钟 更新于 10 月 29 日

一、什么是软件测试

软件测试是验证软件是否符合预期的过程。本质上做两件事:

  1. 找 bug:代码行为和设计不符的地方
  2. 防回归:今天修好的 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)

被测函数:一个计算订单总价的函数,有折扣、有税费。

js
// 订单计算函数(被测代码)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;}

测试用例:

js
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 好测试长什么样

一个好的单元测试应该:

  1. 一个 it 只测一件事——出了问题一眼就知道哪里坏了
  2. 起名说人话——it('空购物车返回 0') 比 it('test empty case') 清楚 100 倍
  3. 不依赖外部——不连数据库、不读文件、不调第三方 API
  4. 跑一遍毫秒级——慢了就没人愿意跑

四、集成测试

单元测试搞定了"一个函数能不能算出正确结果",但真实业务中函数要连数据库、调 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。

js
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(慢)。推荐两个时机:

  1. PR 合并前——CI 里自动跑一遍 E2E
  2. 发布前——手动触发一次完整 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 配置:

yaml
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

十、记住这三句话

  1. 测试不是给 QA 写的,是给半年后的自己写的。
  2. 一个人开发,也值得有 CI。
  3. 如果你从没被"上次改的代码搞坏了这次的功能"坑过,那你还没写够代码。

前端、后端、部署、客服都是我一个人。写这里的东西都是真踩过的坑,不是抄来的教程。