《软件测试》课程笔记
L1 - 软件测试概述
IMPORTANCE
- 经典航空事故案例、千年虫等 说明了软件测试的重要性
CONCEPTS & GLOSSARY
- 软件测试:给程序一定的输入 对比预期输出和实际输出值
- RIPR Model 用于评估一个错误如何才能被观测到
- Reachability
- Infection
- Propagation
- Revealability
- Myers 准则
- 越早测试越好
- 避免让开发者本身进行测试
- 测试用例=输入数据+预期输出(不可以是自己随意判断的)
L2 - 基本理论与方法
Non-Execution Based Testing
- 静态测试:不运行程序 通过检查代码、文档等来发现错误 比如程序员自己检查、代码审查、走查等
- 好处是可以在开发早期进行 尽早发现问题
Execution Based Testing
- 动态测试:运行程序 通过测试用例来发现错误 包括黑盒测试、白盒测试、灰盒测试等
- 黑盒测试:不关心程序内部结构 只关心输入输出是否符合预期 需要一些测试用例设计技巧来保证覆盖率 比如等价类划分、边界值分析等
- 白盒测试:关心程序内部结构 通过代码覆盖率等指标来评估测试质量 通常需要关注代码的执行路径来设计测试用例 保证覆盖一些边缘情况 比如语句覆盖、分支覆盖等
Formal Verification
- 形式化验证:通过数学方法来证明程序的正确性 通常用于安全性要求较高的场景 比如航空航天、金融等
Other Methods
- Def-Use Test:检查变量的定义和使用是否正确
- Mutation Test:通过对程序进行小的变异来检查测试用例的质量
- Regression Test:检查修改代码后是否引入了新的错误
- Fault Stastic:统计错误的分布情况 比如标记重捕法来评估错误的百分比 用于证明测试的有效性
- Reliability Analysis:通过模型来评估软件的可靠性
- Clean Room:?
L3 - 功能测试
边界测试
- 边界测试:测试输入输出的边界情况 即最大最小值、略大略小值以及正常值共 4N+1 个用例
- 依据:单缺陷假设 即假设一个缺陷往往只会导致一个错误
- 健壮性测试:在边界测试的基础上增加无效的异常值 共 6N+1 个用例
- 最坏情况测试:拒绝单缺陷假设 每个维度取 5 个值进行笛卡尔积 共 5^N 个用例 如果需要健壮性 变为 7^N 个用例
- 随机测试:随机生成输入值进行测试 避免人为的测试偏见
等价类测试
- 等价类划分:将输入域划分为若干等价类 每个等价类只需要测试一个代表值
- 弱一般等价类(单缺陷):测试数取等价类最多的维度 可以保证覆盖每个维度的所有划分
- 强一般等价类(多缺陷):测试数取每个维度划分数的乘积 可以保证覆盖所有可能的组合
- 弱健壮等价类:在弱一般等价类的基础上增加无效值 即每个维度的最大划分和最小划分的外部值 当需要定位错误时 可以增加无效值组合
- 强健壮等价类:在强一般等价类的基础上增加无效值的所有组合
基于决策表的测试
- 决策表:描述输入输出之间的关系 包括条件部分和动作部分
- 决策表测试:通过决策表来设计测试用例 保证覆盖所有可能的组合 有时会因为条件太多而导致测试用例数过多
测试效率
- 测试用例数:边界测试<等价类测试<决策表测试 同理精细度也是如此
指导方针
- 变量是物理量:边界值测试、等价类测试
- 变量是逻辑量:等价类测试、决策表测试
- 变量独立:边界值测试、等价类测试
- 变量不独立:决策表测试
- 单缺陷假设:边界值测试、健壮性测试
- 多缺陷假设:最坏情况测试、决策表测试
- 包含大量例外处理:健壮性测试、决策表测试
L4 - 结构测试
- 结构测试即根据程序内部逻辑结构进行的白盒测试 其目的是提高测试覆盖率
路径测试
- 程序图:描述程序的控制流程的有向图 点表示语句块 边表示控制流 即 i->j 表示 i 执行后 j 可以立马执行
- DD-路径图:一个程序可能会被表示为不同的程序图 但是都可以简化为唯一的 DD-路径图
- DD-路径:程序图中最小独立的路径 可能有以下几种情况
- 单个源点或单个汇点
- 单个入度>=2 或出度>=2 的节点
- 单个入度=1 且出度=1 的节点
- 长度>=2 的最长路径
- DD-路径:程序图中最小独立的路径 可能有以下几种情况
- 测试覆盖指标(部分)
- 语句覆盖:执行每个语句块至少一次
- 分支覆盖:执行每个 DD-路径至少一次 即每个判断的真假至少执行一次
- 条件覆盖:每个判断的不同条件至少执行一次
- 多条件覆盖:每个判断的所有条件组合至少执行一次
- 分支/条件覆盖:分支覆盖+条件覆盖
- 无穷路径覆盖:所有可能的路径至少执行一次
- 循环覆盖:单循环、嵌套循环、级联循环等
- McCabe 圈数
- 基路径:类似于基向量 是可以表示程序所有路径的某一组独立路径
- 基路径必须从起始点到终止点
- 一条基路径必须包含一条其他所有基路径未包含的边或节点
- 对于循环 基路径应该包含不执行循环和执行一次循环的路径
- 圈复杂度:用于定量计算基路径数量的复杂度指标
- 公式:V(G) = E - N + 2P 其中 E 为边数 N 为节点数 P 为连通分量数(非强连通有向图 如果是强连通 2P 改为 P)
- 基路径的线性组合只具有数学意义 即加法是路径 i 后接路径 j 乘法是路径的重复执行 减法是去除路径中的边
- 基路径的寻找基于图的遍历算法 比如 dfs 和 bfs
- 每一条基路径可以作为一个测试用例 对于所有基路径进行测试后 可以认为所有路径都被覆盖了
- 基本复杂度:一般而言 一个单元模块的复杂度<10
- 基路径:类似于基向量 是可以表示程序所有路径的某一组独立路径
数据流测试
- 路径测试关注的是程序的结构 相对的 数据流测试关注的是程序中数据及其使用的角度
- 定义-使用路径测试
- 定义节点:变量的输入语句、赋值语句、循环控制语句等 即写入变量的语句
- 使用节点:变量的输出语句、判断语句、循环控制语句等 即读取变量的语句
- 谓词使用:语句为谓词语句 比如 if (x > 0)
- 计算使用:语句非谓词语句 比如 y = x + 1
- 定义-使用路径:以定义节点为起点 使用节点为终点的路径
- 定义清除路径:以定义节点为起点 使用节点为终点 且没有其他定义节点的路径
- 一个即是定义节点又是使用节点的语句一般不被认为是 def-use 路径
- 定义使用路径测试的覆盖指标
- 全定义准则:每个定义节点到一个使用节点的定义清除路径
- 全使用准则:每个定义节点到所有使用节点以及后续节点的定义清除路径(后续节点:比如谓词使用后续的逻辑语句)
- 全谓词使用准则:每个定义节点到所有谓词使用的定义清除路径 若无谓词使用 至少有一个一个计算使用的定义清除路径
- 全计算使用准则:每个定义节点到所有计算使用的定义清除路径 若无计算使用 至少有一个一个谓词使用的定义清除路径
- 全定义-使用路径准则:每个定义节点到所有使用节点以及后续节点的定义清除路径 包括有一次环路和无环路的路径
- 基于程序切片的测试
- 程序切片:给定一个程序和一个变量集合 切片是程序的一个子集 该子集包含了所有对变量做出贡献的语句(及使用或定义了该变量的语句)
- 定义为 S(v,s) 代表语句 s 处变量 v 所有可能依赖的语句集合 需要注意后续循环语句的影响 还需要注意控制流的影响
- 程序切片将程序分为了不同依赖的部分 从而可以更好地定位错误
测试效率
- 漏洞与冗余:漏洞是未被测试到的路径 冗余是被多次测试到的路径
- s 是结构单元总数 m 为测试用例个数 n 为覆盖到的结构单元个数
- 覆盖率 C = n/s
- 冗余率 R = m/s
- 净冗余率 NR = m/n
L5 - 集成测试与系统测试
- 集成测试:需要了解软件的结构和功能 是结构化测试方法 由开发者进行
- 系统测试:不需要了解软件的内部结构 只需要了解软件的功能和需求 是非结构化测试方法 由测试人员或用户进行
集成测试
基于功能分解的集成测试
- 自顶向下:从主控模块开始逐步向下集成 为了定位错误 往往用桩程序代替下层模块 完成测试后再逐步用真实程序替换
- 自底向上:从最底层模块开始逐步向上集成 为了定位错误 往往用驱动器代替上层模块 完成测试后再逐步用真实程序替换
- 三明治集成:自顶向下和自底向上的结合
- 大爆炸测试:不分层次 所有单元一次性集成测试
基于调用图的集成测试
- 调用图:描述模块之间的调用关系
- 成对集成:相当于测试调用图的每一条边
- 相邻集成:将相邻节点(直接前驱和后继)作为集合进行测试
基于路径的集成测试
- 模块执行路径:以源节点(其他模块转移到本模块的语句)开始 以汇节点(转移到其他模块的语句)结束 且中间没有其他汇节点的路径 代表的是模块内部的执行路径
- MM-路径:模块间的执行路径 即模块内执行路径的组合 描述了模块之间的调用关系
- 圈复杂度:V(G) = E - N + 2P 其中 E 为边数 N 为节点数 P 为连通分量数 双向边算两条边
系统测试
- 系统的功能可以用输入输出来分析 因此可以使用黑盒测试方法进行测试
线索
- 系统级线索是指系统输入到输出的路径(功能的最小单元)
- 原子系统功能(ASF):在系统层可以观察到的端口输入和输出事件的行动
- 事件静止特性:开始于一个输入事件 结束于一个输出事件 遍历多个 MM-路径
- 事件序列原子化特性:ASF 不可再细分
- 系统的 ASF 图是一个有向图 系统线索则是 ASF 图中源到汇的路径
基本概念
- 数据
- 操作
- 设备
- 事件
- 线索:从源 ASF 到汇 ASF 的路径
建立系统 ASF 图
- ASF 相当于一个个状态 整个系统的状态转移图就是系统的 ASF 图
系统测试指导方针
- 齐夫定律:80%的错误来自 20%的功能
L6 - 单元测试工具
L7-L8 - GUI 与性能测试
L9 - 回归测试
回归测试概述
- 回归测试的本质是选出尽可能少的测试用例来验证所有修改代码的正确性 而非生成新的测试用例集
- 回归测试:在软件修改后 重新执行之前的测试用例 以验证修改的正确性和影响
回归测试策略
- 回归测试包的选择(效率和有效性的 balance)
- 再测试所有测试用例 代价高
- 选择部分测试用例 包括基于风险、基于操作剖面等方法
- 本质上 应该选择那些实际执行路径中的代码被修改的测试用例
- 测试用例库:所有测试用例会被保存 进行维护和管理
- 基线测试用例库:针对某个稳定发布版本的测试用例集合 可以作为后续代码更新后进行回归测试的基准
回归测试的步骤
- 识别软件被修改的部分
- 从原基线测试用例库 T 中排除不适用的测试用例 形成新的测试用例库 T0
- 依据特定策略从 T0 中选择测试用例进行测试
- 如果必要 生成新的测试用例集 T1 用于补充测试 T0 无法覆盖的部分
- 用 T1 测试修改后的软件
- 2-3 检验了修改是否破坏了原有功能 4-5 检验了修改工作本身是否正确
选择回归测试用例的方法
- 基于执行轨迹:
- 对于程序 P 和测试用例集 T 找到 T 中每一个用例在 P 中的实际执行路径
- 对 P 的 CFG 中每个节点 解析出执行轨迹的测试向量(测试向量即节点被哪些测试用例覆盖 比如{t1,t3})
- 为 P 和 P’的 CFG 的每个节点建立语法树 找到语法树发生变化的节点
- 对于每个变化的节点 找到其对应的测试向量 合并得到最终的测试用例集 T’
- 基于测试最小化(也就是除去 T0 中的冗余):
- 选定测试实体 比如函数、语句 划定{ei}为程序 P 的实体集合
- 对于测试集 T 的每个用例 找到其覆盖的实体集合{ei}_t
- 找到最小覆盖集 T’ 使得覆盖所有实体集合{ei}_t
- 基于测试优先级:当某些测试用例特别重要时 不能轻易删除 比如优先级是基于覆盖率
- 同样划分实体集合{ei}
- 为每一个实体集合{ei}_t 计算覆盖个数 作为优先级
- 按照优先级从高到低选择测试用例 选择的个数基于测试资源和产品质量的权衡
L10 - 变异测试
变异测试概述
- 变异测试:通过对程序进行小的变异来评估测试用例的有效性 提供了除覆盖率以外的另一个评估方法
- 核心观点:如果程序修改后 仍能通过测试用例的测试 则说明测试用例对于修改的部分没有覆盖到 这种情况下要么是测试用例设计不合理 要么是程序本身的错误 因此我们希望变异后程序无法通过测试用例的测试
- 变异体:对程序进行小的变异后 形成的新程序
- 有效测试用例:能够发现变异体的测试用例 称测试用例 t 可以杀死变异体 P’ 一旦某一个变异体 P’不能被任何一个测试用例杀死 则说明测试用例集不够充分 我们称这个变异体存活过了变异测试
- 变异体有可能与原程序是等价的 这种情况下测试用例无法发现变异体
- 一旦某一个变异体 P’不能被任何一个测试用例杀死 我们称这个变异体存活过了变异测试 说明测试集 T 不够充分 需要继续添加测试用例
变异测试的步骤
- 生成变异体集合 M={m1, m2, …, mn}
- 为每个变异体执行测试用例集 T 查看变异体是否存活
- 统计存活变异体的数量 k1 若 k=k1 则 T 充分 否则计算变异分数 MS=k1/(k-e) 其中 e 为等价变异体的数量
- 若 MS=1 则说明测试用例充分 需要使用新的变异体或其它测试方法增强测试用例集
- 若 MS<1 则说明测试用例不充分 可以通过添加新的测试用例来增强测试用例集 重复添加直至 MS=1
变异体的设计
- 测试用例的充分性是对于变异体而言的 因此不同的变异体可能会导致不同的结果
- 变异体模拟的是程序员经常会犯的错误 因此变异体的设计需要考虑程序员的思维方式 比如符号、变量、常量的替换
- 变异体的指标
- 可达性:测试用例必须要覆盖到变异语句 否则一定结果一致
- 传染性:变异语句会引起状态变化
- 传播性:执行完变异语句后的不一致状态会导致程序的输出不一致
- 等价变异体通常只能一个一个进行查看才能确定 一般实践时估计等价变异体占比为 5%
- 变异操作符
- 变量替换:x*y -> x*z
- 运算符替换:x+y -> x-y
- 关系符替换:x>y -> x<y
- 多阶变异体:经过多次变异后形成的变异体 实践中一般只考虑一阶变异体 这是因为资源限制和耦合效应
- 耦合效应(Coupling Effect):一般高阶变异可以由多个低阶变异体组合而成 因此低阶变异体的测试用例集往往可以覆盖高阶变异体
- 变异测试一般针对单元测试 系统测试的变异测试成本太高
L11 - O-O 测试
- O-O 测试:面向对象的测试方法
O-O 测试的特点
- O-O 测试的单元:在结构化程序设计中 单元往往是一个函数或过程 而在 O-O 程序设计中 单元往往是一个类或对象
- 类作为测试单元时 存在类的继承和多态性的问题 解决的方法是扁平类 即对有继承的类进行扩充 包含所有父类的属性和方法
- O-O 测试的层次
- 操作/方法测试
- 类测试
- 集成测试
- 系统测试
类测试
- 以方法为测试单元 等价于结构化程序设计中的函数测试 可以使用传统的黑盒和白盒测试方法
- 以类为测试单元 重点在于类的消息序列 即对象之间的消息传递
面向对象的集成测试
- 集成测试的 UML 支持:类图、用例图等是进行测试的有力工具
- 顺序图是事件发生的时序 可以认为是一个完整的 MM-路径
- O-O 数据流集成测试框架:事件驱动和消息驱动的 Petri 网——EMDPN
L12 - 嵌入式系统测试
嵌入式系统的实时特性
- 嵌入式系统一般用电池供电 要求低功耗
- 为了保证实时性 一般需要测试 WCET(Worst Case Execution Time)即最坏情况执行时间
测试技术
- 黑盒测试
- 白盒测试:基于路径、基于数据流
- 回归测试
- 交叉测试:不同平台间的测试
考试
- 路径测试 2 道
- 数据流测试 1 道
- 回归测试 1 道
- 变异测试 1 道
《软件测试》课程笔记
https://nwdnys1.github.io/2025/02/19/归档/ST/