软件的特征 Software Characteristics
Complexity 复杂性
Evolution 演化性
Artifact, reflecting human intelligence. 软件是一种人工制品,体现了人类智能。
一些事实 The Facts
只有 32% 的软件项目被认为是成功的,也就是功能完整、按时交付、没有超预算。
软件失效每年给美国经济造成 595 亿美元的损失。
平均每千行代码有 1–5 个 bug。
在成熟软件中也是如此;原型系统中的 bug 通常更多,可能超过每千行 10 个。
3500 万行代码。
发布时已有 6.3 万个已知 bug。
平均每千行代码 2 个 bug。
就像软件无处不在一样,bug 也无处不在。
一些软件的bug的原因
- 整数溢出,所以当关卡数从 255 再加 1 时,就会发生溢出,导致第 256 关显示异常。
- 没有正确转换不同的度量单位标准,一个模块输出英制单位,另一个模块按国际单位制理解。
- 除零错误,任何数除以 0 都是非法运算,程序如果没有处理这种情况,就可能崩溃或停止运行。
- 设备软件中对竞态条件处理不当。
- C 语言代码中的 break 语句使用错误。
- 算术溢出,从 64 位浮点数转换为 16 位有符号整数时发生异常。
- 火箭项目中没有进行回归测试。
- 订单是否可撤销的判断条件存在缺陷。
说明:软件缺陷不仅影响程序本身,还可能影响金融、医疗、航空、基础设施和公共安全。
Bug 的出现是不可避免的。
减少缺陷的方法 Approaches to reduce faults
人工代码审查
通过人工阅读代码来发现缺陷。
局限性
- 很难评估审查进度和效果。
- 可能遗漏很多缺陷或 bug。
代码审查通常涉及两个角色:
- 代码作者
- 审查者
减少缺陷的自动化方法
Static Analysis 静态分析
Testing 测试
Verification 验证
静态分析
静态分析:通过扫描代码中的可疑模式,识别软件中的特定问题,例如内存泄漏。
局限性
- 能够发现的问题类型有限。
- 可能产生误报。误报:工具报告显示有问题,但实际上这里没有缺陷。
测试
测试:向软件输入数据并运行它,观察其行为是否符合预期。
局限性
- 不可能覆盖所有可能的执行情况。
- 需要测试预言机,也就是判断输出是否正确的依据。
- 输入域可能是无限的。
验证
形式化验证:考虑程序所有可能的执行情况,并用形式化方法证明程序是否正确。
局限性
- 很难得到严格的形式化规格说明。
- 大多数现实世界中的程序验证成本太高,难以完整证明。
为什么要进行测试?
- 测试与代码审查相比:测试比代码审查更可靠。
- 测试与静态检查相比:测试误报更少,并且适用于更多类型的问题。
- 测试与形式化验证相比:测试可扩展性更强,适用于更多程序。
- 投入越多,通常收获越多,收益相对线性。
例如,形式化验证更加耗时、更加困难,但实际发现的 bug 数量未必比测试多。
课程大纲
结构化测试 数据流测试 随机测试 等价类测试 组合测试
缺陷、错误与失效 Fault, Error & Failure
软件缺陷:软件中的静态缺陷,也就是 defect。Software Fault
软件错误:由某个缺陷引起的、不正确的内部状态。Software Error
软件失效:相对于需求或其他预期行为描述而言,软件表现出的外部错误行为。Software Failure
Bug:对 fault 或 failure 的非正式说法。
软件缺陷 Software Fault
软件缺陷:软件中的静态缺陷,也就是 defect 或 bug。
public static void CSta(int[] numbers)
{
int length = numbers.length;
double mean, sum;
sum = 0.0;
for (int i = 1; i < length; i++) // i=0
{
sum += numbers[i];
}
mean = sum / (double) length;
System.out.println("mean: " + mean);
}这里的缺陷是:i 应该从 0 开始,而不是从 1 开始
软件错误 Software Error
软件错误:由某个缺陷引起的、不正确的内部状态。
for (int i = 1; i < length; i++) // i=0
{
sum += numbers[i];
}这里测试输入[3, 4, 5],但是实际执行sum = 4 + 5。
软件失效 Software Failure
软件失效:相对于需求或预期行为而言,软件表现出的外部错误行为。
测试输入为[3, 4, 5],我们希望他sum = 3 + 4 + 5,mean = 4。
但是实际上,sum = 4 + 5,mean = 3。
因此产生了失效。
也就是说,当内部错误状态最终表现为错误输出时,就形成了 failure。
PIE 模型
Propagation, Infection, Execution
传播、感染、执行
Execution / Reachability:执行 / 可达
Infection:感染
Propagation:传播
PIE 模型用于解释:为什么一个软件缺陷不一定会导致软件失效。一个 fault 想要最终表现成 failure,通常需要经历:
- 执行到缺陷位置;
- 缺陷导致程序内部状态错误;
- 错误状态传播到外部输出。
程序中包含缺陷的位置必须被执行到。
程序状态必须变成错误状态。
被感染的错误状态必须传播出去,导致程序某些输出错误。
一个测试用例可能根本没有执行到缺陷所在的位置!
一个测试即使执行到了缺陷,也不一定会产生错误状态!
比如当输入[0, 4, 5],sum = 0 + 4 + 5,结果没有影响。
一个错误状态也不一定会传播成错误输出!
例如:
public static void CSta(int[] numbers)
{
int length = numbers.length - 1;
double mean, sum;
sum = 0.0;
for (int i = 0; i < length; i++)
{
sum += numbers[i];
}
mean = sum / (double) length;
System.out.println("mean: " + mean);
}我们测试输入[3, 5, 4],sum = 3 + 5 + 4 = 12,mean = 12 / 3 = 4
错误程序的计算:
sum = 3 + 5 = 8
mean = 8 / 2 = 4
尽管sum 和 length 都不正确,但是最终输出仍然是mean = 4

public static int mean(int[] numbers) {
int sum = 0;
// Fault: 这里应该是 i = 0,但错误写成了 i = 1
for (int i = 1; i < numbers.length; i++) {
sum += numbers[i];
}
return sum / numbers.length; // 整数除法
}| 测试 | 输入 | 是否执行 fault | 是否产生 error | 是否产生 failure |
|---|---|---|---|---|
| t1 | [0, 4, 5] | 是 | 否 | 否 |
| t2 | [1, 2, 2] | 是 | 是 | 否 |
| t3 | [3, 4, 5] | 是 | 是 | 是 |
补充说明:
t2 的内部 sum 已经错了,但由于整数除法截断,最终输出没错,所以 error 没有传播成 failure。
术语 Terminology
基本概念
- 测试用例 Test Case
- 测试输入 Test Input
- 测试数据 Test Data
- 测试预言机 Test Oracle
- 期望输出 Expected output
- 测试套件 Test suite
- 测试脚本 Test script
- 测试驱动 Test driver
测试预言机
对于给定输入,软件应该产生的期望输出。
测试预言机是测试用例的一部分。
自动化测试中最困难的问题之一:测试预言机生成。
Test oracle 的作用是判断程序输出到底对不对。
自动生成输入有时不难,难的是自动知道“正确答案应该是什么”。
测试夹具
被测软件在运行测试时所依赖的固定状态,也称为测试上下文。例如:
向数据库加载一组特定且已知的数据。
准备输入数据,并设置或创建伪对象、模拟对象。
测试夹具就是测试执行所需要的固定环境。
Mock 对象是模拟对象,它们以可控方式模仿真实对象的行为。
测试套件与测试脚本 Test suite & Test script
测试套件是一组测试用例的集合。
这些测试用例通常共享类似的前置条件和配置。
通常可以按顺序一起运行。
可以针对不同目的设计不同测试套件。
测试驱动程序 Test driver
测试驱动程序是一种软件框架,可以加载一组测试用例或测试套件。
它还可以处理测试配置,并比较期望输出与实际输出。
eg: 比如 JUnit、pytest 这类框架,在某种意义上就承担了 test driver 的作用
它们负责组织测试、运行测试、收集结果、判断通过或失败。
测试充分性 Test adequacy
我们不可能总是使用所有测试输入,那么应该选择哪些输入?什么时候可以停止测试?
我们需要一种策略来判断测试是否已经足够。
测试充分性准则:一种规则,用于判断一组测试数据对于某个软件来说是否足够。
测试覆盖率
测试覆盖率是一种度量,用于评估代码中有多少比例被测试到了。
语句覆盖率 Statement coverage
分支覆盖率 Branch coverage
测试 vs. 调试
测试是通过执行测试并观察失效来揭示 bug。
调试是通过定位、理解并修正缺陷来修复 bug。
Testing and Debugging
测试负责发现问题,调试负责修复问题。
验证 vs. 确认
验证 Verification
评估一个产品、服务或系统是否符合规定、需求、规格说明或强加条件。它通常是一个内部过程。
评估产品、服务或系统是否符合规定、需求、规格说明或约束条件。
它通常是内部过程。
确认 Validation
确保一个产品、服务或系统满足客户及其他相关利益方的需求。它通常涉及外部客户的验收和适用性判断。
确保产品、服务或系统满足客户和其他相关方的真实需要。
它通常涉及外部客户的验收和适用性判断。
验证:我们是否在正确地构建产品?
验证:假设我们应该构建 X,那么我们的软件是否在没有 bug 或缺口的情况下实现了目标?
确认:我们构建的是不是正确的产品?
确认:X 是我们本来就应该构建的东西吗?它是否满足高层需求?
静态测试 vs. 动态测试
Static Testing vs. Dynamic Testing
静态测试
静态测试:不执行程序。
动态测试
执行程序。
代码审查、静态分析属于静态测试;
单元测试、集成测试、系统测试通常属于动态测试。
黑盒测试 vs. 白盒测试
黑盒测试
不依赖源代码。
白盒测试
基于源代码。
灰盒测试 Gray-box Testing
灰盒测试人员部分了解系统的内部结构,包括可以访问内部数据结构的文档,以及所使用算法的相关信息。
灰盒测试介于黑盒和白盒之间:不是完全不知道内部实现,也不是完全基于源码进行测试。
测试层级 Testing Level
Unit testing 单元测试
Module testing 模块测试
Integration testing 集成测试
System testing 系统测试
V 模型

测试过程
简单来说,测试过程是:先根据软件结构或模型制定测试需求,再生成测试用例和测试脚本,执行后得到结果,最后根据通过或失败进行评估和反馈。
缺陷定位
- 测试输入
- 期望输出
- 实际输出
- 修改后是否通过
- 修改是否足够小且合理
