功能测试计划模板:把风险映射到证据

0阅读14分钟

测试计划不是列命令

我见过太多测试计划长这样:第一步跑单测,第二步跑集成测试,第三步跑 E2E,第四步上线。这不叫测试计划,这叫 CI 流水线截图。

真正有用的测试计划回答的问题只有一个:如果这个功能坏了,哪条测试或验收会发现? 答不上来的风险,要么补证据,要么明确接受。

测试计划的重点不是写「运行测试」,而是说明哪些风险由哪些证据覆盖。需求越复杂,越需要提前设计验证方式。好测试计划能让团队知道什么时候可以放心发布,而不是靠「感觉差不多了」来决定上线时间。

这篇文章给出一个可以落地的功能测试计划模板,覆盖从风险识别到证据映射的完整流程。模板不是死板的表格,而是一套思路——帮你把「我觉得应该测这些」变成「这些风险已经被这些证据覆盖」。

理论基础:风险驱动测试与测试金字塔

风险驱动测试(Risk-Based Testing)

风险驱动测试的核心逻辑不复杂:并非所有功能都需要同等力度的测试。团队应将有限的时间和人力优先投入到「最可能出错」且「出错代价最大」的区域。

这个概念来自软件工程中的 FMEA(失效模式与影响分析)方法。FMEA 用三个维度给风险打分:

维度含义评分范围
严重度(Severity)风险发生后的影响程度1-10
发生概率(Likelihood)风险出现的可能性1-10
可检测性(Detectability)缺陷在到达用户前被发现的难易程度1-10

三者相乘得到风险优先级数(RPN):

RPN = 严重度 × 发生概率 × 可检测性

一个支付网关的风险可能是:严重度 9 × 概率 6 × 可检测性 7 = 378。一个「帮助页面链接错误」的风险可能是:严重度 2 × 概率 3 × 可检测性 2 = 12。RPN 越高,测试优先级越高,资源分配越多。

测试金字塔的分层逻辑

Mike Cohn 提出的测试金字塔是另一块基石。它的核心法则是「等级越高,测试次数就越少」:

测试层级数量占比执行速度覆盖范围反馈时效
单元测试最多(~70%)毫秒级单个函数/模块即时
集成测试中等(~20%)秒级模块间交互、API 契约分钟级
E2E 测试较少(~8%)分钟级完整用户流程十分钟级
浏览器验收最少(~2%)人工视觉、交互、真实体感按需

web.dev 的测试策略文章指出:「测试越能反映软件的使用方式,你对测试结果的可信度就越高。」但顶层的全链路检查因依赖完整环境且耗时,资源消耗极大。所以不能把测试计划写成「所有东西都用 E2E 测一遍」。

风险 × 层级的映射关系

把风险和层级结合起来,核心判断标准是:

  • 纯函数和状态模型 → 单元测试
  • API 契约和模块协作 → 集成测试
  • 用户关键路径(注册、下单、支付) → E2E 测试
  • 视觉、交互、文案细节 → 浏览器验收
  • 边界条件和异常状态 → 视具体场景决定层级

不要用一个测试层级解决所有问题,也不要为了覆盖率数字好看而写无意义的测试。

案例一:支付功能上线前的测试计划

场景

我在一个电商项目负责支付模块重构。旧系统从 Stripe 切换到自研支付网关,涉及订单创建、支付回调、退款流程三个核心链路。团队有 2 个后端、1 个前端、1 个 QA,距离上线还有两周。

翻车

第一次评审测试计划时,QA 同事交上来的计划是这样的:

## ❌ 反面示例:没有风险映射的测试计划
 
### 测试范围
- 支付流程
- 订单管理
- 退款功能
- 用户界面
 
### 测试类型
- 单元测试:覆盖核心逻辑
- 集成测试:覆盖 API
- E2E 测试:覆盖主要流程
- 回归测试:覆盖历史 bug

这份计划的问题在于:它只列了「测什么」和「用什么测」,但没有说「为什么这些风险用这个层级覆盖」。如果有人在评审会上问「退款回调失败了谁来发现」,没有人能指着这份计划说清楚。

修复

重写后的测试计划从风险清单开始,逐条映射到测试层级:

## ✅ 正面示例:风险驱动的测试计划
 
### 风险清单与证据映射
 
| 风险项 | RPN | 覆盖层级 | 具体证据 | 负责人 |
|--------|-----|---------|---------|--------|
| 支付回调签名验证失败 | 9×5×6=270 | 单元+集成 | `payment-callback.test.ts` + 契约测试 | 后端 A |
| 重复支付导致重复扣款 | 10×4×7=280 | 单元+E2E | `deduplication.test.ts` + 支付 E2E 场景 3 | 后端 B |
| 退款金额超过原订单 | 9×3×5=135 | 单元 | `refund-amount.test.ts` | 后端 A |
| 支付超时后状态不一致 | 8×6×8=384 | 集成+E2E | `timeout-reconcile.test.ts` + 超时 E2E 场景 | 后端 B |
| 支付页 loading 状态展示错误 | 3×7×3=63 | 浏览器验收 | 人工验收 checklist #2 | 前端 |
| 退款到账后邮件通知未发送 | 5×5×4=100 | 集成 | `notification.test.ts` | 后端 A |
 
### 不覆盖范围及原因
- 第三方支付网关内部逻辑(依赖 Stripe 自身的 SLA)
- 极端并发场景(首期流量预估不涉及,二期补压测)

每个风险都有 RPN 评分、对应的测试层级和具体的测试文件/场景编号。QA 不用再猜「我是不是漏了什么」,研发也能对照计划检查自己的单测是否命中了风险项。

代码示例:风险矩阵的结构化定义

// ✅ 正面:用类型定义驱动风险矩阵,确保每条风险都有覆盖
interface RiskItem {
  id: string
  description: string
  severity: number    // 1-10
  likelihood: number  // 1-10
  detectability: number // 1-10
  rpn: number
  testLayer: 'unit' | 'integration' | 'e2e' | 'browser' | 'manual'
  testFile: string    // 对应的测试文件或验收项
  owner: string
}
 
const paymentRisks: RiskItem[] = [
  {
    id: 'PAY-001',
    description: '支付回调签名验证失败',
    severity: 9,
    likelihood: 5,
    detectability: 6,
    rpn: 9 * 5 * 6, // 270
    testLayer: 'unit',
    testFile: 'tests/payment-callback.test.ts',
    owner: '后端 A',
  },
  {
    id: 'PAY-002',
    description: '重复支付导致重复扣款',
    severity: 10,
    likelihood: 4,
    detectability: 7,
    rpn: 10 * 4 * 7, // 280
    testLayer: 'e2e',
    testFile: 'tests/e2e/payment-dedup.spec.ts',
    owner: '后端 B',
  },
]
 
// 发布前校验:每条高风险项必须有对应测试
function validateCoverage(risks: RiskItem[], threshold = 150) {
  return risks
    .filter((r) => r.rpn >= threshold)
    .map((r) => ({
      id: r.id,
      covered: fileExists(r.testFile),
      rpn: r.rpn,
    }))
}
// ❌ 反面:风险项散落在文档里,没有结构化,发布前无法自动校验
// 以下信息只存在于 Confluence 页面的一个表格里
const risks = [
  { name: '支付回调问题', test: '应该测一下' },
  { name: '重复扣款', test: 'E2E 覆盖' },
  // 没有 RPN、没有文件路径、没有负责人
  // 发布前无法程序化检查覆盖情况
]

案例二:用户权限系统的分层验证

场景

一个 SaaS 后台要新增「多角色权限」功能。管理员可以创建自定义角色、分配权限、邀请成员。权限模型涉及 RBAC(基于角色的访问控制),有 5 种预设角色和无限自定义角色,权限粒度到菜单级和操作级。

翻车

最初团队的测试策略是「全部用 E2E 覆盖」。结果:

  • E2E 用例超过 200 条,单次执行 45 分钟
  • 权限矩阵稍有变动就要改十几条 E2E
  • CI 频繁超时,开发者开始跳过测试本地提交
  • 上线后还是漏了「自定义角色无法看到已分配菜单」的 bug——因为 E2E 只测了预设角色

修复

重新按层级拆分后,测试计划变成了:

### 权限系统分层测试策略
 
| 验证目标 | 测试层级 | 数量 | 说明 |
|---------|---------|------|------|
| 权限判断纯函数 | 单元测试 | ~80 | `hasPermission(role, action, resource)` 的各种组合 |
| 角色 CRUD API | 集成测试 | ~25 | 创建/编辑/删除角色的 API 契约和数据库一致性 |
| 权限分配后的 API 响应 | 集成测试 | ~30 | 分配权限后接口返回的数据是否正确 |
| 预设角色的关键路径 | E2E 测试 | ~10 | 管理员创建角色 → 分配权限 → 成员登录 → 验证可见菜单 |
| 自定义角色的关键路径 | E2E 测试 | ~5 | 自定义角色的完整创建到使用流程 |
| 菜单权限的视觉呈现 | 浏览器验收 | ~3 | 权限变更后的菜单显隐、禁用态 |

权限判断的纯函数用单元测试覆盖所有边界,API 层用集成测试验证数据一致性,只有最核心的用户路径用 E2E。这样权限矩阵调整时,只需要改单测和集成测试,E2E 基本不用动。

代码示例:单测覆盖权限判断逻辑

// ✅ 正面:纯函数单测,覆盖各种边界条件
import { hasPermission } from '@/core/permissions'
 
describe('hasPermission', () => {
  it('超级管理员拥有所有权限', () => {
    expect(hasPermission('admin', 'delete', 'user')).toBe(true)
    expect(hasPermission('admin', 'create', 'role')).toBe(true)
  })
 
  it('自定义角色只能访问已分配的权限', () => {
    const role = {
      name: 'editor',
      permissions: [
        { action: 'read', resource: 'article' },
        { action: 'write', resource: 'article' },
      ],
    }
    expect(hasPermission(role, 'read', 'article')).toBe(true)
    expect(hasPermission(role, 'delete', 'article')).toBe(false)
    expect(hasPermission(role, 'read', 'user')).toBe(false)
  })
 
  it('未分配任何权限的角色不能访问任何资源', () => {
    const emptyRole = { name: 'guest', permissions: [] }
    expect(hasPermission(emptyRole, 'read', 'article')).toBe(false)
  })
 
  it('权限判断区分 action 粒度', () => {
    const role = {
      name: 'viewer',
      permissions: [{ action: 'read', resource: 'report' }],
    }
    expect(hasPermission(role, 'read', 'report')).toBe(true)
    expect(hasPermission(role, 'export', 'report')).toBe(false)
  })
})
// ❌ 反面:把权限判断逻辑放在组件里测试,依赖完整渲染
// 测试慢、脆弱、出错时不知道是权限逻辑问题还是渲染问题
import { render, screen } from '@testing-library/react'
import { AdminPanel } from '@/components/AdminPanel'
 
test('admin can see delete button', async () => {
  render(<AdminPanel userRole="admin" />)
  // 依赖整个组件树渲染
  // 如果组件结构变了,测试就要改
  // 实际上权限判断逻辑和渲染无关
  const btn = await screen.findByText('删除')
  expect(btn).toBeVisible()
})

案例三:表单功能的边界条件覆盖

场景

一个用户注册流程,包含手机号验证、验证码发送、密码强度校验、邀请码校验四个步骤。产品需求文档写了 2 页,里面有大量边界条件散落在不同段落。

翻车

开发照需求写完了代码,QA 按需求文档逐条验收也通过了。上线后第一周收到一堆反馈:

  • 手机号带空格能提交成功
  • 验证码过期后重新发送,旧验证码仍然有效
  • 密码里包含手机号时没有拦截
  • 邀请码过期后输入,页面卡住没有提示

这些边界条件其实在需求文档里都提到了,但散落在不同段落,开发看漏了,QA 也没有系统性地覆盖。

修复

测试计划增加了一个专门的「边界条件映射」模块,把需求文档中的每个边界条件提取出来,对应到具体的测试用例:

### 注册流程边界条件映射
 
| 边界条件 | 来源 | 测试层级 | 测试文件 | 状态 |
|---------|------|---------|---------|------|
| 手机号前后有空格 | 需求 1.3 节 | 单元测试 | `phone-validation.test.ts` | ✅ |
| 验证码过期后旧码不可用 | 需求 2.1 节 | 集成测试 | `sms-code-expire.test.ts` | ✅ |
| 密码不能包含手机号 | 需求 3.2 节 | 单元测试 | `password-strength.test.ts` | ✅ |
| 邀请码过期提示 | 需求 4.1 节 | E2E | `register-invite.spec.ts` | ✅ |
| 同一手机号 60 秒内重复发送 | 需求 2.3 节 | 集成测试 | `sms-rate-limit.test.ts` | ✅ |

代码示例:边界条件测试

// ✅ 正面:针对每个边界条件写独立测试,名称即文档
describe('手机号验证', () => {
  it('拒绝前后带空格的手机号', () => {
    expect(validatePhone(' 13800138000')).toBe(false)
    expect(validatePhone('13800138000 ')).toBe(false)
  })
 
  it('拒绝非 11 位号码', () => {
    expect(validatePhone('1380013800')).toBe(false)
    expect(validatePhone('138001380001')).toBe(false)
  })
 
  it('拒绝非大陆号段', () => {
    expect(validatePhone('12345678901')).toBe(false)
  })
 
  it('接受合法手机号', () => {
    expect(validatePhone('13800138000')).toBe(true)
    expect(validatePhone('15912345678')).toBe(true)
  })
})
 
describe('密码强度校验', () => {
  it('密码不能包含手机号', () => {
    const result = validatePassword('abc13800138000xyz', '13800138000')
    expect(result.valid).toBe(false)
    expect(result.reason).toContain('手机号')
  })
 
  it('密码长度至少 8 位', () => {
    expect(validatePassword('short', '13800138000').valid).toBe(false)
    expect(validatePassword('longEnough1!', '13800138000').valid).toBe(true)
  })
})
// ❌ 反面:一个巨型测试函数覆盖所有场景,出错时定位困难
test('注册流程验证', () => {
  // 手机号、验证码、密码、邀请码全搅在一起
  expect(register({ phone: '13800138000', password: 'test1234' })).toBeTruthy()
  expect(register({ phone: ' 13800138000' })).toBeFalsy()
  expect(register({ phone: '13800138000', inviteCode: 'EXPIRED' })).toBeFalsy()
  // 哪个条件失败?为什么失败?不知道
  // 新增边界条件时只能往这个 test 里堆
})

从风险到证据的决策流程

下面这张图展示了拿到一个新功能需求后,如何从风险识别一步步走到测试证据的落地:

流程图画布 · 115%
Mermaid 流程图加载中...

这个流程的关键点在于:风险分级不是做完就扔的一次性动作,而是在发布前会再次校验的持续过程。每次代码变更都可能引入新风险,测试计划应该跟着更新。

测试计划的完整模板结构

把所有内容汇总,一个完整的功能测试计划模板如下:

模板结构

模块内容填写时机
功能目标一句话说清楚这个功能做什么需求评审时
关键路径用户必须走完的核心流程列表需求评审时
风险清单可能出错的场景、边界条件技术方案设计时
RPN 评分每条风险的严重度 × 概率 × 可检测性技术方案设计时
单元测试范围覆盖哪些纯函数、状态模型、工具函数开发启动时
集成测试范围覆盖哪些 API 契约、模块协作开发启动时
E2E 测试范围覆盖哪些用户关键路径开发启动时
浏览器验收哪些视觉/交互细节需要人工确认开发启动时
不覆盖范围明确列出不在本轮测试范围内的内容及原因评审时确认
发布校验发布前跑一次覆盖检查,确认所有高风险项有证据发布前
发布后监控上线后关注的错误率、性能指标、告警阈值发布前

对比:好测试计划和坏测试计划

维度坏测试计划好测试计划
开头「测试范围:支付、订单、用户」「风险清单:重复扣款、回调签名、超时状态不一致」
测试类型「需要单元测试和 E2E 测试」「PAY-001 风险由 payment-callback.test.ts 覆盖」
覆盖边界不写不覆盖范围明确列出不覆盖内容及原因
发布判断「测试通过了可以上线」「RPN ≥ 200 的风险全部有对应测试且通过」

对比:不同测试层级的选择标准

判断维度适合单元测试适合集成测试适合 E2E 测试适合浏览器验收
被测对象纯函数、工具函数API、数据库操作多服务协作流程视觉、交互体验
外部依赖无或全部 mock依赖真实数据库/缓存依赖完整环境依赖真实浏览器
执行速度< 100ms< 5s< 5min人工时间
维护成本按需
变更敏感度内部实现变了就要改接口变了才改页面流程变了才改视觉变了就要看

对比:不同团队的测试计划粒度

团队规模功能复杂度建议计划粒度工具选择
1-3 人单功能点Markdown 清单 + RPN 评分代码仓库内 test-plan.md
4-8 人跨模块功能风险矩阵表 + 层级映射代码仓库 + 项目管理工具
8+ 人跨团队功能结构化风险表 + 自动化校验专用测试管理平台

测试计划的风险评估方法

五维风险识别框架

评估一个功能的风险时,我从五个维度入手:

维度关注点典型问题
业务风险影响收入、用户体验、竞争优势这个功能坏了会影响多少用户?
技术风险复杂逻辑、新技术、第三方依赖这块代码有多少分支?用了不熟悉的库吗?
数据风险数据一致性、并发、迁移涉及钱或库存吗?有并发写入吗?
安全风险鉴权、注入、数据泄露涉及用户敏感信息吗?输入有校验吗?
运维风险部署回滚、监控告警、降级方案上线后能快速回滚吗?有告警吗?

不是每个功能都需要五维全评。对于小功能,只看业务风险和技术风险就够了。对于涉及钱、权限、数据迁移的功能,五维都要过一遍。

代码示例:风险评分计算器

// ✅ 正面:风险评分结构化,可以程序化校验
type RiskLevel = 'critical' | 'high' | 'medium' | 'low'
 
function classifyRisk(rpn: number): RiskLevel {
  if (rpn >= 300) return 'critical'
  if (rpn >= 200) return 'high'
  if (rpn >= 100) return 'medium'
  return 'low'
}
 
function requiredLayers(level: RiskLevel): string[] {
  switch (level) {
    case 'critical':
      return ['unit', 'integration', 'e2e', 'monitoring']
    case 'high':
      return ['unit', 'integration']
    case 'medium':
      return ['unit']
    case 'low':
      return ['manual']
  }
}
 
// 使用示例
const risk = classifyRisk(270) // 'high'
const layers = requiredLayers(risk) // ['unit', 'integration']
// ❌ 反面:风险等级靠口头约定,没有量化标准
// 「这个风险比较高,多测一下」
// 多高算高?多测是测多少?每个人理解不同
function testRisk(riskName: string) {
  if (riskName.includes('支付')) return '测仔细点'
  if (riskName.includes('显示')) return '随便看看'
  // 没有量化,无法自动化校验
}

代码示例:发布前覆盖校验脚本

// ✅ 正面:发布前自动校验所有高风险项都有对应测试
import { existsSync } from 'node:fs'
import { join } from 'node:path'
 
interface RiskEntry {
  id: string
  rpn: number
  testFile: string
  testPassed: boolean
}
 
function preReleaseCheck(risks: RiskEntry[], rootDir: string) {
  const highRisks = risks.filter((r) => r.rpn >= 200)
  const results = highRisks.map((r) => {
    const filePath = join(rootDir, r.testFile)
    return {
      id: r.id,
      rpn: r.rpn,
      fileExists: existsSync(filePath),
      testPassed: r.testPassed,
    }
  })
 
  const uncovered = results.filter((r) => !r.fileExists)
  const failed = results.filter((r) => r.fileExists && !r.testPassed)
 
  if (uncovered.length > 0) {
    console.error(`❌ 未覆盖的高风险项: ${uncovered.map((r) => r.id).join(', ')}`)
  }
  if (failed.length > 0) {
    console.error(`❌ 测试未通过的高风险项: ${failed.map((r) => r.id).join(', ')}`)
  }
 
  const canRelease = uncovered.length === 0 && failed.length === 0
  console.log(canRelease ? '✅ 高风险项全部覆盖且通过' : '🚫 发布阻塞')
  return canRelease
}
// ❌ 反面:发布前靠人工确认,容易遗漏
// 在 Slack 里问一句「测试都过了吗」然后有人说「过了」就上线
// 没有程序化校验,没有审计记录
async function manualReleaseCheck() {
  // 打开 Confluence 页面
  // 逐条对比测试计划和测试结果
  // 手动标记哪些通过哪些没通过
  // 祈祷没有漏看
  console.log('人工检查完毕,应该可以发布')
}

发布后监控:测试计划的延伸

测试计划在发布后并没有结束。线上监控是测试计划的最后一层证据。

监控维度关注指标告警阈值示例
错误率接口 5xx 比例> 1% 触发 P2 告警
延迟P99 响应时间> 2s 触发 P3 告警
业务指标支付成功率、注册转化率较昨日同期下降 > 10%
用户反馈客服工单数量功能相关工单 > 5 条/小时
资源使用CPU、内存、数据库连接CPU > 80% 持续 5 分钟

把线上监控纳入测试计划,是因为有些风险只有生产环境才能暴露。测试环境的流量模式、数据规模、网络条件都和线上有差异。监控不是替代测试,而是测试的兜底。

发布前检查清单

以下清单按阶段分组,可以在每次功能发布前逐项确认:

需求阶段

  • 功能目标是否用一句话描述清楚
  • 关键用户路径是否已列出
  • 边界条件和异常场景是否已从需求文档中提取

风险评估阶段

  • 是否完成五维风险识别(业务/技术/数据/安全/运维)
  • 每条风险是否已计算 RPN 评分
  • RPN ≥ 200 的高风险项是否已标记
  • 是否明确了「不覆盖范围」及原因

测试设计阶段

  • 每条高风险项是否映射到至少两层测试
  • 单元测试是否覆盖所有纯函数和状态模型
  • 集成测试是否覆盖所有 API 契约和模块交互
  • E2E 测试是否仅覆盖最核心的用户路径(不超过 15 条)
  • 浏览器验收是否覆盖视觉和交互敏感场景

开发阶段

  • 测试文件是否按风险矩阵中的路径命名和存放
  • 单测是否能在 30 秒内跑完
  • 集成测试是否不依赖外部服务的真实响应(使用 mock 或 sandbox)

发布前校验

  • 是否运行覆盖校验脚本确认所有高风险项有测试
  • 所有自动化测试是否全部通过
  • 浏览器验收是否已完成并签字确认

发布后

  • 监控告警是否已配置(错误率、延迟、业务指标)
  • 回滚方案是否已准备并验证
  • 灰度策略是否已确定(按比例还是按用户分组)

常见问题与误区

误区一:测试计划 = 测试用例列表。 测试计划回答的是「为什么测这些」和「怎么知道可以发布」,不是列出 200 条测试步骤。

误区二:覆盖率越高越好。 追求 100% 覆盖率会让团队写出大量脆弱且无意义的测试。覆盖率是参考指标,不是目标。风险覆盖率(高风险项有多少被测试覆盖)比代码覆盖率更有价值。

误区三:E2E 测试越多越安全。 E2E 测试维护成本高、执行慢、容易因为无关改动而失败。一个稳定的 E2E 套件应该只覆盖最核心的用户路径,通常在 10-15 条以内。

误区四:测试计划写完就不用改了。 需求变更、技术方案调整、线上事故复盘都可能引入新风险。测试计划应该随开发进程持续更新。

误区五:小功能不需要测试计划。 判断标准不是功能大小,而是风险大小。「修改按钮文案」不需要,但「修改按钮显示逻辑」就需要。

小结

测试计划的本质是风险管理。好的测试计划有三

参考资料

评论 0

0 / 1000