软件测试
AI WARNING
未进行对照审核。
软件为何需要测试
软件缺陷可能导致从轻微不便到灾难性后果的各种问题。软件测试不仅是技术活动,更是保障系统可靠性、安全性乃至生命财产安全的关键环节。
验证与确认
软件测试是软件质量保障的重要方法,广义上属于「验证与确认」(V&V)活动的一部分。
- 验证(Verification):确保我们「正确地构建了产品」。它关注的是开发过程的正确性,检查开发者是否正确地使用技术来建立系统,确保系统能在预期环境中按照技术要求正确运行。
- 例如:检查需求文档中的书写错误、发现设计思路的不完备性、审查代码中的编程错误等。
- 确认(Validation):确保我们「构建了正确的产品」。它关注的是最终产品是否满足用户需求和规格说明。
- 例如:需求文档内容是否反映用户真实意图、设计能否跟踪到需求、测试是否覆盖需求、代码是否按照需求与设计的要求编写等。
V&V 的手段
软件开发中的 V&V 主要通过两种手段进行:
- 静态分析:在软件不运行的情况下,依据开发文档、模型或其他制品(如原型)进行检查,以完成验证与确认任务。例如代码审查、文档评审。
- 动态测试(即通常所说的软件测试):在软件运行时,考察其运行时表现(如输入/输出、性能、可靠性),以完成验证与确认任务。
graph TD
A[需求开发] --> B(体系结构设计);
B --> C(详细设计);
C --> D(构造);
D --> E{测试};
E --> F[移交与维护];
subgraph V&V 活动
G(静态分析<br/>验证与确认) -.-> A;
G -.-> B;
G -.-> C;
G -.-> D;
D -- 开发者测试 --> E;
E -- 测试者测试 --> D;
end
style G fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#ccf,stroke:#333,stroke-width:2px
软件测试的目标
软件测试具有双重目标:
- 有效性测试:向开发者和用户展示软件满足了需求,表明软件产品是一个合格的产品。它通常使用用户期望的方式来测试系统。
- 缺陷测试:找出软件中的缺陷和不足。这是软件测试中更重要的目标,只有发现了缺陷的测试才是成功的测试。
发现缺陷是成功的标志
在短期内,生产者的生产水平是相对稳定的,因此不同产品的质量和缺陷数量也应相似。如果缺陷测试未能发现缺陷或发现数量远少于历史数据,并不一定说明产品质量高,反而可能是测试活动不合格。反之,如果发现的缺陷远多于历史数据,可能说明产品质量较差,但也可能说明测试活动表现出了较高水平。核心在于:发现尽可能多的缺陷的测试才是成功的。
测试用例
软件测试通过执行测试用例来进行。
- 每个测试用例是一组精心设计的输入数据与预期输出结果的组合。
- 输入数据:可以是外部传入的数据,也可以是系统内部的状态数据。
- 预期结果:可以是系统输出的数据,也可以是系统的运行表现(如成功、失败、性能指标等)。
graph LR
TC[测试用例] -- 输入数据 --> SUT(被测试系统);
TC -- 预期结果 --> C{比较};
SUT -- 输出结果 --> C;
C -- 相同 --> Pass(通过测试);
C -- 不同 --> Fail(存在缺陷);
桩程序与驱动程序
在测试,特别是单元测试和集成测试中,常常需要将被测部件(如一个函数、模块或类)与其依赖的其他部件隔离开,以便独立测试。这时就需要使用桩程序和驱动程序。
- 桩程序(Stub):是被测试部件调用的其他模块的模拟体。它通常只包含最简单的逻辑,例如返回一个固定的、预设的值,或者根据简单规则返回数据,以满足被测试部件的调用需求。
- 驱动程序(Driver):是调用被测试部件并为其提供测试环境的模拟体。它负责设置测试环境、输入测试用例的输入数据、调用被测试部件、获取输出结果,并与预期结果比较,最终给出测试结论。
graph TD
subgraph 驱动程序
direction TB
D1[设置环境]
D2[输入测试数据]
D3[调用被测部件]
D4[获取输出]
D5[比较结果]
D6[给出结论]
D1 --> D2 --> D3 --> D4 --> D5 --> D6
end
subgraph 桩程序
direction TB
S1[接收被测部件请求]
S2[按规则返回结果]
S1 --> S2
end
subgraph 驱动程序和桩
direction TB
Driver[驱动程序] --> TestedComponent(被测试部件);
TestedComponent --> Stub[桩程序];
end
style Driver fill:#cde,stroke:#333,stroke-width:2px
style Stub fill:#dec,stroke:#333,stroke-width:2px
缺陷、错误与失败
理解这三者之间的关系对于缺陷分析至关重要:
缺陷、错误与失败的关系链
- 缺陷(Defect/Fault/Bug,故障):系统代码或设计中存在的不正确的地方。例如,计算时可能出现除以零的情况,或者逻辑判断条件错误。
- 错误(Error):当系统执行到有缺陷的代码,并且特定条件被触发时,系统内部状态会进入一个不符合预期且不稳定的状态,这就是错误。例如,除零操作实际执行,导致程序内部产生一个异常状态。
- 失败(Failure):错误的发生最终导致软件功能失效,表现为用户可见的、不符合规格说明的行为。例如,系统某个功能输出不正确、异常终止、不满足时间或空间限制等。
通常的链条是:缺陷 错误 失败。缺陷可能长期潜伏,直到特定条件激活它,引发错误,最终导致用户可见的失败。
测试的层次
软件测试通常按照测试对象的范围和阶段划分为不同的层次。
单元测试
- 单元测试,也称为模块测试,是对软件设计的最小可测试单元进行正确性检验的测试工作。
- 在过程化编程中,一个单元通常是一个函数或过程。
- 在面向对象编程中,一个单元通常是一个类的方法或整个类。
- 通常由程序员在编码过程中或完成后进行,需要构建桩程序和驱动程序将被测单元与其他部分隔离。
- 测试用例的设计可以基于规格说明(黑盒),也可以基于代码结构(白盒)。
graph TD
Driver[驱动程序] --> Unit(程序单元);
Unit --> Stub1[桩程序 1];
Unit --> Stub2[桩程序 2];
TestData[测试用例<br/>(来自工程师经验、接口规格、代码结构等)] --> Driver;
style Driver fill:#cde,stroke:#333,stroke-width:2px
style Stub1 fill:#dec,stroke:#333,stroke-width:2px
style Stub2 fill:#dec,stroke:#333,stroke-width:2px
集成测试
- 集成测试,也称为组装测试,是将已通过单元测试的模块按设计要求组合起来,对模块间的接口和交互进行正确性检验的测试工作。
- 通常在单元测试之后、系统测试之前进行。
- 也需要桩程序和驱动程序,其使用依赖于集成策略。
- 常见的集成策略:
- 大爆炸集成:所有模块一次性集成。
- 增量集成:逐步将模块集成起来测试。
- 自顶向下集成:从主控模块开始,逐层向下集成其调用的子模块,子模块用桩程序替代。
- 自底向上集成:从最底层的模块开始,逐层向上集成调用它的模块,上层模块用驱动程序模拟。
- 持续集成:频繁地集成代码并自动运行测试。
自顶向下集成示例
graph TD
R(顶层模块 R)
A(模块 A)
B(模块 B)
C(模块 C)
D(模块 D)
F(模块 F)
E(模块 E)
Driver[驱动程序] --> R;
R --> A;
A --> B;
A --> F((桩 F));
B --> E((桩 E));
R --> C;
C --> D((桩 D));
subgraph 集成顺序
direction TB
S1[① R,A]
S2[② R,A,F]
S3[③ R,A,F,B]
S4[④ R,A,F,B,E]
S5[⑤ R,A,F,B,E,C]
S6[⑥ R,A,F,B,E,C,D]
end
S1 --> S2 --> S3 --> S4 --> S5 --> S6;
style Driver fill:#cde
style F fill:#dec
style E fill:#dec
style D fill:#dec
- 优点:较早验证系统主要控制流程和高层接口。
- 缺点:底层模块的问题发现较晚,桩程序开发工作量可能较大。
自底向上集成示例
graph TD
R((驱动 R))
A((驱动 A))
B(模块 B)
C(模块 C)
D(模块 D)
F((驱动 F))
E(模块 E)
R --> F;
R --> A;
A --> E;
A --> B;
B --> C;
F --> D;
subgraph 集成顺序
direction TB
S1[① C,B]
S2[② C,B,D]
S3[③ C,B,D,A]
S4[④ C,B,D,A,E]
S5[⑤ C,B,D,A,E,R]
S6[⑥ C,B,D,A,E,R,F]
end
S1 --> S2 --> S3 --> S4 --> S5 --> S6;
style R fill:#cde
style A fill:#cde
style F fill:#cde
- 优点:较早发现底层模块的缺陷,桩程序需求少。
- 缺点:系统整体行为验证较晚。
系统测试
- 系统测试是将经过集成测试的软件,作为计算机系统的一个部分,与系统中其他部分(如硬件、其他软件、数据、人员)结合起来,在实际运行环境下对整个系统进行的一系列严格有效的测试,以发现软件潜在的问题,保证系统按规格说明正确运行。
- 更关注不符合需求的缺陷和需求自身的内在缺陷。
- 不依赖桩程序和驱动程序,但可使用测试工具自动化测试过程。
- 根据测试目标的不同,系统测试可细分为:
- 功能测试:验证系统是否满足需求规格说明中定义的功能。
- 非功能性测试:验证系统是否满足性能、可靠性、易用性、安全性等非功能需求。
- 验收测试:由用户或其代表在模拟或实际环境中进行的测试,以确认系统是否满足用户需求并可接受。
- 安装测试:验证软件在目标环境中的安装过程和结果。
其他测试类型(按测试目标)
除了上述主要层次,还有许多按特定目标划分的测试类型,例如:
- 性能测试:评估系统在特定负载下的响应时间、吞吐量、资源利用率等。
- 易用性测试:评估用户学习和使用软件的难易程度、效率和满意度。
- 可靠性测试:评估系统在规定条件下和规定时间内无故障运行的能力。
- 安全性测试:验证系统保护数据和功能免受未授权访问或恶意攻击的能力。
- 回归测试:在软件修改(如缺陷修复、功能增强)后,重新执行部分或全部已测试过的用例,以确保修改没有引入新的缺陷或导致原有功能退化。
测试技术
测试技术是帮助软件测试人员设计和选择测试用例的方法。目标是以尽可能小的代价发现尽可能多的缺陷,因为穷尽测试(测试所有可能的输入和路径)在实践中是不可行的。
随机测试
随机测试(Ad hoc Testing)
- 基于软件工程师的直觉、经验和对类似程序的理解来选择测试用例。
- 例如,测试
int add(int x, int y)
函数时,有经验的工程师会考虑普通输入(如 x=100, y=20)和可能导致溢出的输入(如 x=2147483640, y=100)。 - 虽然不是最优技术,因为它发现缺陷的几率可能较低,但有时能发现其他技术难以发现的特定缺陷。
基于规格的技术——黑盒测试
黑盒测试将测试对象视为一个「黑盒子」,完全不考虑其内部结构和实现细节,仅根据其规格说明(需求文档、功能描述等)来设计测试用例,检查输入和输出数据是否符合预期。
- 等价类划分
- 将程序的输入域划分为若干个互不相交的子集,称为等价类。假设同一等价类中的所有输入数据对于揭露程序中的某个特定错误来说是等效的。
- 从每个等价类中选取一个或少数代表性数据作为测试用例。
- 等价类分为:
- 有效等价类:符合规格说明的、合理的输入数据集合。
- 无效等价类:不符合规格说明的、不合理的输入数据集合。
- 设计测试用例时,应同时覆盖有效和无效等价类。
等价类划分示例:getChange(double payment, double total)
假设 getChange
方法用于计算找零,规格为:
payment
total
且payment
0,total
0 时,返回payment - total
。payment
total
时,提示付款不足。payment
0 时,提示付款金额无效。
输入 payment
和 total
的等价类可以划分为:
- 有效:
payment = 100, total = 50
(预期输出 50) - 无效(付款不足):
payment = 50, total = 100
(预期输出「付款不足」) - 无效(付款金额无效):
payment = -10, total = 20
(预期输出 「付款金额无效」)
- 边界值分析
- 是对等价类划分方法的补充。经验表明,错误最容易发生在等价类的边界上或边界附近。
- 选取等价类边界上的值、略大于边界的值、略小于边界的值作为测试用例。
边界值分析示例:getChange(double payment, double total)
继续上面的例子,考虑 payment >= total
的边界:
payment = total
(如payment=50, total=50
,预期输出 0)payment = total + epsilon
(如payment=50.01, total=50
,预期输出 0.01)payment = total - epsilon
(如payment=49.99, total=50
,预期输出「付款不足」)
考虑payment > 0
的边界:payment = 0
(预期输出 「付款金额无效」)payment = epsilon
(如payment=0.01, total=0
,预期输出 0.01)payment = -epsilon
(预期输出 「付款金额无效」)
- 决策表测试
- 适用于具有复杂逻辑条件和相应操作的场景。
- 决策表由条件桩、动作桩、条件项和动作项四部分组成,每一列(规则)代表一种条件组合及其对应的动作。
- 每个规则可以设计为一个测试用例。
决策表示例:礼品赠送
规则:
prePoint < 1000 && postPoint >= 1000
GiftLevel = 1
prePoint < 2000 && postPoint >= 2000
GiftLevel = 2
prePoint < 5000 && postPoint >= 5000
GiftLevel = 3
测试用例:
prePoint=500, postPoint=1500
预期GiftLevel=1
prePoint=500, postPoint=2500
预期GiftLevel=2
prePoint=500, postPoint=5500
预期GiftLevel=3
- 状态转换测试
- 适用于具有明确状态和状态间转换的系统(如协议实现、GUI 界面流等)。
- 首先为测试对象建立状态图,描述其状态集合、输入事件集合以及输入事件导致的状态转换。
- 然后基于状态图设计测试用例,覆盖所有(或重要)状态和转换。
状态转换示例:简易销售流程
stateDiagram-v2
测试用例可以设计为:
- `Initial -> addMember -> Member`
- `Initial -> addSalesLineItem -> LineItem -> total -> Payment -> endSale -> [*]`
基于代码的技术 —— 白盒测试
白盒测试,也称为结构测试或逻辑驱动测试,将测试对象视为一个「透明盒子」,测试人员了解其内部结构和实现细节,并根据程序内部逻辑来设计测试用例,以检查代码的覆盖程度和逻辑正确性。
常用的白盒测试覆盖标准:
- 语句覆盖
- 目标是确保被测试对象的每一行可执行程序代码都至少执行一次。
- 是最弱的覆盖标准。
- 条件覆盖(或分支覆盖的变种)
- 目标是确保程序中每个判断(如
if
,while
中的条件表达式)的每个可能结果(真和假)都至少出现一次。 - 比语句覆盖强,因为它能发现因条件判断错误而导致分支未执行的问题。
- 目标是确保程序中每个判断(如
- 路径覆盖
- 目标是确保程序中每条独立的执行路径都至少执行一次。
- 是最强的覆盖标准之一,但对于包含循环或大量分支的复杂程序,路径数量可能非常庞大,难以完全覆盖。
白盒测试示例:Customer.getBonus()
1 | public class Customer { |
程序流程图(简化)
graph TD
A["a: preBonus = getBonus()"] --> B{b: cashPayment?};
B -- True --> C[c: preBonus += consumption] --> D{d: vip?};
B -- False --> D;
D -- True --> E[e: preBonus *= 1.5] --> G[g: return preBonus];
D -- False --> F[f: preBonus *= 1.2] --> G;
- 语句覆盖:
- 用例 1:
cashPayment=true, vip=true
(路径: a-b-c-d-e-g)。覆盖 a, b, c, d, e, g。 - 用例 2:
cashPayment=true, vip=false
(路径: a-b-c-d-f-g)。覆盖 a, b, c, d, f, g。
- 用例 1:
- 条件覆盖:
- 判断
b (cashPayment)
: true, false - 判断
d (vip)
: true, false - 用例 1:
cashPayment=true, vip=true
(b=T, d=T) - 用例 2:
cashPayment=false, vip=false
(b=F, d=F)
- 判断
- 路径覆盖(独立路径):
- a-b(T)-c-d(T)-e-g
- a-b(T)-c-d(F)-f-g
- a-b(F)-d(T)-e-g
- a-b(F)-d(F)-f-g
- 用例 1:
cashPayment=true, vip=true
- 用例 2:
cashPayment=false, vip=false
- 用例 3:
cashPayment=true, vip=false
- 用例 4:
cashPayment=false, vip=true
特定测试技术
针对特定类型的软件或技术,还需要专门的测试技术,例如:
- 面向对象的测试(关注封装、继承、多态带来的测试挑战)
- 图形用户接口(GUI)测试
- 基于 Web 的测试
- 并发程序测试
- 实时系统测试
测试活动
典型的软件测试过程包括以下活动:
- 测试计划
- 在具体测试活动开始前进行,明确测试的工作范围、资源与成本、基本策略、进度安排等。
- 包括单元测试计划、集成测试计划、系统测试计划等。
- 计划需要评审以保证质量。
- 测试设计
- 核心是设计有效的测试用例集合。
- 综合考虑测试层次、被测对象特点、测试目标,选择合适的测试技术。
- 测试执行
- 选择合适的测试工具(可以减少重复劳动,提高效率)。
- 严格按照测试用例执行,记录测试结果。
- 测试用例日志:记录每个测试用例的执行情况(ID、种类、条件、期望结果、实际结果、测试对象 ID 等)。
- 缺陷报告:记录发现的缺陷(ID、发现日期、测试版本、测试用例、期望结果、实际结果、状态、严重性、优先级、缺陷类型、备注、复现步骤等)。
- 测试评价
- 测试执行结束后,评价测试结果,确定测试是否成功。
- 「成功」通常表示软件按期望运行,且没有重大的非期望结果。
- 对发现的缺陷进行分析和排错(隔离、标识、描述)。
- 发布测试报告(如 IEEE 829 标准定义的测试报告格式),内容包括测试总结、详细结果、缺陷分析、对产品质量的评估、对后续工作的建议等。
测试度量
通过度量来评估测试过程的有效性和软件产品的质量。
- 缺陷数据:
- 按引入缺陷的阶段分类:系统需求缺陷、设计缺陷、编码缺陷。
- 按缺陷的影响力分类:严重缺陷、一般缺陷、无影响缺陷。
- 测试覆盖率:衡量测试完整性的指标。
- 需求覆盖率 = 被测试的需求数量/需求总数
- 模块覆盖率 = 被测试的模块数量/模块总数
- 代码覆盖率 = 被测试的代码行数/代码行总数(或语句覆盖率、分支覆盖率、路径覆盖率等)
覆盖率与程序规模
一般来说,系统越复杂(代码行数越多),达到高测试覆盖率所需的测试用例数量也越多,且完全覆盖的难度也越大。例如,一个 100 万行代码的系统,达到 25% 的代码覆盖率可能就需要 5 万个测试用例。
项目实践中的角色(参考)
在团队项目中,测试相关的角色和职责可能包括:
- 软件测试人员/程序员:共同完成测试工作和程序修正。
- 质量保障人员(首席软件测试人员):测试工作的负责人和协调人,分析测试度量数据,组织测试评价,编写测试报告。
- 项目管理人员:控制项目任务分配与进度,监控任务执行,审核测试制品。
- 文档编写人员:组织讨论,确定文档规范。