黑盒测试与白盒测试
引言:软件测试的两种视角
在软件测试中,我们通常从两个截然不同的视角来审视被测软件,这两种视角催生了两种核心的测试方法论:白盒测试和黑盒测试。
- 白盒测试(White-box Testing):将软件看作一个透明的盒子。测试人员能够洞察其内部的逻辑结构、代码实现和数据流。测试的目的是验证内部工作流程是否正确。因此,它也被称为结构化测试(Structural Testing)或逻辑驱动测试(Logic-driven Testing)。
- 黑盒测试(Black-box Testing):将软件看作一个不透明的黑盒子。测试人员完全不关心其内部实现,只关注软件的外部功能和行为。测试的目的是验证软件是否满足需求规格说明书(SRS)中定义的功能。因此,它也被称为功能测试(Functional Testing)或数据驱动测试(Data-driven Testing)。
此外,测试活动可以根据是否运行被测软件,分为静态测试和动态测试。
graph TD
A(软件测试) --> B(白盒测试);
A --> C(黑盒测试);
subgraph 白盒测试
B --> B1(静态白盒测试);
B --> B2(动态白盒测试);
end
subgraph 黑盒测试
C --> C1(静态黑盒测试);
C --> C2(动态黑盒测试);
end
B1 --- D1[代码评审<br>同行评审<br>代码走查<br>代码审查];
B2 --- D2[逻辑覆盖<br>路径覆盖<br>数据流覆盖];
C1 --- D3[文档审查];
C2 --- D4[等价类划分<br>边界值分析<br>因果图];
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#ccf,stroke:#333,stroke-width:1px
style C fill:#9cf,stroke:#333,stroke-width:1px
白盒测试
白盒测试的核心在于深入软件内部,根据其代码结构和逻辑来设计测试用例。
静态白盒测试:代码评审
静态测试是在不实际运行代码的情况下,通过人工或工具来分析和检查软件的设计、代码和文档。其中,代码评审(Code Review)是最主要的形式。
代码评审
代码评审是一个系统化的过程,旨在通过集体智慧发现软件中的错误、缺陷和不符合规范之处。它不仅能发现问题,还能促进知识共享和提升团队整体代码质量。
一个有效的代码评审过程通常具备以下特征:
- 目标明确:核心是发现问题,对事不对人,营造积极的批评氛围。
- 规则清晰:遵循预定规则,如限定评审时长、代码量,明确各参与者职责。
- 充分准备:评审效果很大程度上取决于会前准备。参与者应提前熟悉代码,带着问题参会。
- 产出报告:会议结束后需产出正式的书面报告,记录发现的问题及其位置,并跟踪后续修复。
代码评审主要有以下四种形式,其正式程度递增:
- 同行评审(Peer Review)
- 也称好友评审,是一种非正式的评审方式。
- 通常由两到三名开发者互相检查对方的代码,形式灵活,类似于「看看你的,看看我的」。
- 尽管非正式,但依然是发现低级错误和逻辑问题的有效手段。
- 代码走查(Code Walkthrough)
- 形式更正式一些。由代码的原作者向一个小型评审小组(通常 3-5 人)逐行逐功能地讲解代码逻辑。
- 评审小组成员提前获取代码并进行预习。
- 这是一种以作者为中心的讲解和答疑过程,有助于暴露作者思路中的盲点。
- 代码审查(Code Inspection)
- 最正式、最严格的评审形式。
- 代码的原作者不参与讲解,仅作为观察者或资源提供者。
- 评审小组由受过专门训练的成员组成(评审员、协调员、记录员等),他们从不同视角(如用户、测试、维护)出发,依据检查清单(Checklist)对代码进行系统性审查。
- 发现的缺陷会被正式记录,并由协调员跟踪验证修复情况。
- 桌面检查(Desk Check)
- 由单人进行的代码检查,可以看作是「一个人的代码走查」。
- 检查者对照错误列表,在头脑中推演测试数据,检查代码逻辑。
- 效率相对较低,因为它违反了「程序员不应检查自己程序」的原则,容易陷入思维定式。
动态白盒测试:逻辑覆盖
动态白盒测试通过运行代码,并根据内部逻辑设计测试用例,以达到特定的覆盖标准(Coverage Criteria)。
穷举测试的困境
理论上,最彻底的白盒测试是路径覆盖,即测试程序中所有可能的执行路径。然而,由于循环和复杂的分支结构,一个看似简单的程序可能包含天文数字的路径。
路径爆炸
一个包含 20 次循环的程序,即使循环体内只有简单的 if-else
,其路径数量也可能达到 (约一百万条)。如果循环次数依赖于输入,路径数将是无限的。因此,穷举测试在实践中是不可行的。
为了在有限的成本和时间内尽可能多地发现错误,我们采用不同的逻辑覆盖标准,它们代表了对程序逻辑测试的详尽程度。
逻辑覆盖的层级
逻辑覆盖标准由弱到强形成一个层级体系。通常,满足更强覆盖标准的测试用例集,也必然满足所有比它更弱的覆盖标准。
graph TD
subgraph 覆盖强度由弱到强
direction LR
A(语句覆盖) --> B(判定覆盖);
A --> C(条件覆盖);
B --> D(判定/条件覆盖);
C --> D;
D --> E(修正判定/条件覆盖<br>MC/DC);
E --> F(条件组合覆盖);
F --> G(路径覆盖);
end
条件覆盖与判定覆盖之间没有绝对的强弱关系,它们关注点不同,可能存在一个测试用例集满足其中一个而不满足另一个的情况。
为了清晰地说明这些覆盖标准,我们使用以下 C++ 代码作为示例:
1 | // 示例函数 |
其控制流图(Control Flow Graph)如下:
graph LR
Start((Start)) --> P1{"A > 1 && B == 0"};
P1 -- True --> S1;
P1 -- False --> P2{"A == 2 || B > 1"};
S1 --> P2;
P2 -- True --> S2;
P2 -- False --> End((End));
S2 --> End;
-
语句覆盖(Statement Coverage)
- 目标:设计测试用例,确保程序中每个可执行语句至少被执行一次。
- 特点:最弱的覆盖标准。它只关心代码是否被执行,不关心逻辑判断的各种可能性。
- 示例用例:
{ A=2, B=0 }
。这个用例会执行 S1 和 S2,覆盖了所有语句。
-
判定覆盖(Decision Coverage)/分支覆盖(Branch Coverage)
- 目标:设计测试用例,确保程序中每个判定的真假分支至少被执行一次。
- 特点:比语句覆盖强,因为它覆盖了所有代码块的入口。
- 示例用例:
{ A=2, B=0 }
:P1 为真,P2 为真。{ A=1, B=1 }
:P1 为假,P2 为假。- 这两个用例覆盖了 P1 和 P2 的所有真/假分支。
-
条件覆盖(Condition Coverage)
- 目标:设计测试用例,确保程序中每个判定中的每个原子条件都至少取得一次真值和假值。
- 特点:它关注判定内部的原子条件,但可能无法覆盖所有分支。
- 示例分析:
- P1:
A > 1
(C1),B == 0
(C2) - P2:
A == 2
(C3),B > 1
(C4)
- P1:
- 示例用例:
{ A=2, B=1 }
:C1(T), C2(F), C3(T), C4(T){ A=1, B=0 }
:C1(F), C2(T), C3(F), C4(F)- 这两个用例使得 C1, C2, C3, C4 都取过真假值。但注意,P1 的真分支从未被执行。
-
条件组合覆盖(Condition Combination Coverage)
- 目标:设计测试用例,确保每个判定中所有原子条件的可能取值组合都至少被执行一次。
- 特点:非常强的覆盖标准,但测试用例数量会随条件数量指数级增长()。
- 示例分析:
- P1 需要覆盖 (T,T), (T,F), (F,T), (F,F) 四种组合。
- P2 需要覆盖 (T,T), (T,F), (F,T), (F,F) 四种组合。
-
判定/条件覆盖(Decision/Condition Coverage)
- 目标:同时满足判定覆盖和条件覆盖。
- 特点:弥补了单独使用判定覆盖或条件覆盖的不足。
- 示例用例:
{ A=2, B=0 }
:P1(T), P2(T); C1(T), C2(T), C3(T), C4(F){ A=1, B=2 }
:P1(F), P2(T); C1(F), C2(F), C3(F), C4(T)- 这两个用例覆盖了所有分支,但 C2 和 C3 的假值还未覆盖。需要补充用例,如
{ A=1, B=1 }
。
-
修正判定/条件覆盖(Modified Condition/Decision Coverage, MC/DC)
- 目标:一种折中的强覆盖标准。要求每个原子条件都能独立地影响判定的最终结果。
- 定义:对于每个原子条件 C,需要找到一对测试用例,满足:
- 这对用例中,条件 C 的取值相反。
- 所有其他条件的取值保持不变。
- 判定的最终结果相反。
- 特点:在航空航天等高安全领域是强制标准(如 DO-178B/C)。它能以较少的测试用例(通常是 N+1 个,最差情况是 2N 个)达到很高的测试强度。
-
路径覆盖(Path Coverage)
- 目标:设计测试用例,覆盖程序中所有可能的执行路径。
- 特点:最强的覆盖标准,但如前所述,由于循环的存在,通常是不可行的。
其他白盒测试技术
- 基本路径测试(Basis Path Testing)
- 一种旨在实现路径覆盖简化的技术。它不要求覆盖所有路径,而是覆盖一个基本路径集。
- 基本路径集是程序中一组线性独立的路径,其数量由环路复杂度(Cyclomatic Complexity)决定。
- 环路复杂度 (其中 E 是边数,N 是节点数),它衡量了程序的逻辑复杂性。
基本路径测试
- 核心思想:旨在通过覆盖程序中一组线性独立的路径,来达到一种高效的路径覆盖。它不追求覆盖所有可能的路径,而是确保程序的每个语句和每个分支至少被执行一次。
- 关键概念:
- 环路复杂度(Cyclomatic Complexity, ):衡量程序的逻辑复杂性,其值等于程序中线性独立路径的数量。
- 计算公式:(其中 是控制流图中的边数, 是节点数)。
- 简化计算(对于结构化程序):(其中 P 是控制流图中的判断节点数,如
if
,while
,for
等)。
- 环路复杂度(Cyclomatic Complexity, ):衡量程序的逻辑复杂性,其值等于程序中线性独立路径的数量。
- 如何应用:
- 绘制程序的控制流图(Control Flow Graph, CFG)。
- 计算环路复杂度 。
- 找出 条线性独立的路径。
- 设计测试用例,确保每条基本路径都被执行到。
- 作用:确保所有逻辑分支都被测试,同时避免测试用例数量的爆炸式增长,提供结构化、系统化的路径覆盖。
示例:
1 | def calculate_grade(score): |
- 环路复杂度 :
- 判断节点数 P = 2 (
score >= 90
,score >= 80
)
- 判断节点数 P = 2 (
- 基本路径集(3 条):
score >= 90
(True) -> "A"score < 90
(False) 且score >= 80
(True) -> "B"score < 90
(False) 且score < 80
(False) -> "C"
- 测试用例:
score = 95
(覆盖路径 1)score = 85
(覆盖路径 2)score = 75
(覆盖路径 3)
- 数据流测试(Data Flow Testing)
- 关注变量的生命周期:定义(definition)、使用(use)和销毁(kill)。
- 测试的目标是覆盖变量从定义点到使用点的路径,旨在发现与变量使用相关的错误(如使用未初始化的变量)。
- 使用分为计算使用(c-use,如
y = x + 1
)和谓词使用(p-use,如if (x > 0)
)。
数据流测试
- 核心思想:关注程序中变量的定义(definition) 和使用(use) 之间的关系。目标是覆盖从变量被赋值(定义)到其值被读取(使用)的所有有效路径。
- 关键概念:
- 定义(def):变量被赋值或初始化(如
x = 10
,read(y)
)。 - 使用(use):变量的值被读取。
- 计算使用(c-use):变量参与计算、赋值或作为输出(如
y = x + 1
,print(x)
)。 - 谓词使用(p-use):变量用于控制流判断(如
if (x > 0)
,while (x < 10)
)。
- 计算使用(c-use):变量参与计算、赋值或作为输出(如
- 销毁(kill):变量超出作用域,或被重新定义(覆盖了之前的值)。
- 定义-使用对(def-use pair):从一个变量的定义点到其使用点之间的一条无定义路径。
- 定义(def):变量被赋值或初始化(如
- 如何应用:
- 识别程序中所有变量的
def
和use
点。 - 找出所有可能的
def-use
路径。 - 设计测试用例,确保这些
def-use
路径被覆盖。
- 识别程序中所有变量的
- 作用:发现与变量使用相关的错误,如使用未初始化的变量、变量值被意外覆盖、变量定义后从未被使用(死代码)等。
示例:
1 | def process_data(a, b): |
- def-use 对:
a
(函数参数定义) ->x = a + b
(c-use)b
(函数参数定义) ->x = a + b
(c-use)x
(第2行定义) ->if x > 10
(p-use)x
(第2行定义) ->y = x * 2
(c-use)x
(第2行定义) ->y = x/2
(c-use)y
(第4行定义) ->return y
(c-use)y
(第6行定义) ->return y
(c-use)
- 测试用例:
process_data(3, 4)
(a=3, b=4 -> x=7)。覆盖x
的p-use
(False)和y = x/2
的c-use
。process_data(6, 7)
(a=6, b=7 -> x=13)。覆盖x
的p-use
(True)和y = x * 2
的c-use
。
- 线性代码序列和跳转测试(LCSAJ Testing)
- 将程序分解为一系列连续执行的代码块(线性序列)和它们之间的跳转。
- 一个 LCSAJ 由(起点,终点,跳转点)三元组定义。测试目标是覆盖所有的 LCSAJ。
线性代码序列和跳转测试
- 核心思想:将程序分解为一系列连续执行的代码块(Linear Code Sequence, LCS),并关注这些代码块之间的跳转(Jump)。它比语句覆盖和分支覆盖更精细,确保每个连续代码块及其所有可能的后续跳转都被测试到。
- 关键概念:
- LCSAJ (Linear Code Sequence And Jump):一个三元组
(起点行号, 终点行号, 跳转目标行号)
。它描述了一个执行路径片段:从起点行号
开始线性执行到终点行号
,然后跳转到跳转目标行号
。 - 线性代码序列(LCS):一段没有内部分支或跳转的连续代码。
- LCSAJ (Linear Code Sequence And Jump):一个三元组
- 如何应用:
- 识别程序中所有的 LCS。
- 识别每个 LCS 结束后的所有可能跳转,从而构建所有 LCSAJ。
- 设计测试用例,确保覆盖所有 LCSAJ。
- 作用:确保程序中的每个「执行流片段」及其后续的「决策或跳转」都被测试。能发现更复杂的控制流错误,例如在特定代码序列后,程序跳转到了错误的位置。
示例:
1 | # 行号 |
- LCSAJ 识别:
- LCS 1: (1, 2) - 从第1行到第2行线性执行。
- LCSAJ 1:
(1, 2, 3)
- 从(1,2) 序列后跳转到第3行(if判断)。
- LCSAJ 1:
- LCS 2: (3, 3) - 第3行是判断点,自身构成一个LCS。
- LCSAJ 2:
(3, 3, 4)
- 从第3行判断为真,跳转到第4行。 - LCSAJ 3:
(3, 3, 6)
- 从第3行判断为假,跳转到第6行。
- LCSAJ 2:
- LCS 3: (4, 4) - 第4行是赋值语句。
- LCSAJ 4:
(4, 4, 7)
- 从第4行执行后跳转到第7行(return)。
- LCSAJ 4:
- LCS 4: (6, 6) - 第6行是赋值语句。
- LCSAJ 5:
(6, 6, 7)
- 从第6行执行后跳转到第7行(return)。
- LCSAJ 5:
- LCS 5: (7, 7) - 第7行是返回语句。
- LCSAJ 6:
(7, 7, END)
- 从第7行执行后程序结束。
- LCSAJ 6:
- LCS 1: (1, 2) - 从第1行到第2行线性执行。
- 测试用例:
check_status(10)
(value > 0): 覆盖 LCSAJ 1, 2, 4, 6。check_status(-5)
(value <= 0): 覆盖 LCSAJ 1, 3, 5, 6。
黑盒测试
黑盒测试完全基于软件的需求和规格说明,不考虑内部实现细节。
黑盒测试的目标
黑盒测试主要用于发现以下类型的错误:
- 功能不正确或遗漏。
- 接口错误(输入无法正确接收或输出不正确)。
- 数据结构或外部数据库访问错误。
- 性能问题。
- 初始化和终止错误。
与白盒测试一样,对输入数据进行穷举测试也是不现实的。因此,我们需要有效的测试用例设计方法。
黑盒测试技术
-
等价类划分(Equivalence Class Partitioning)
- 核心思想:将所有可能的输入数据划分为若干个等价类。假设每个等价类中的任意一个数据在揭示错误方面的作用与该类中其他所有数据相同。
- 分类:
- 有效等价类:符合规格说明、有意义的输入数据集合。
- 无效等价类:不符合规格说明、无意义的输入数据集合。
- 设计步骤:
- 识别输入条件,划分有效和无效等价类。
- 设计测试用例覆盖所有有效等价类。
- 为每个无效等价类单独设计一个测试用例。
-
边界值分析(Boundary Value Analysis)
- 核心思想:大量的错误发生在输入或输出范围的边界上,而不是在其内部。因此,边界值分析是等价类划分的重要补充。
- 原则:针对每个边界,选取正好等于、刚刚小于、刚刚大于边界的值作为测试数据。
- 常见边界:
- 数值范围:最小值、最大值、略低于最小值、略高于最大值。
- 数量:0, 1, 最大数量, 最大数量+1。
- 数据结构:有序集合的第一个和最后一个元素。
登录密码框
需求:密码长度为 6-16 位,包含字母和数字。
- 等价类划分:
- 有效:[6-16 位,含字母数字]
- 无效:[<6 位], [>16 位], [不含字母], [不含数字]
- 边界值分析:
- 长度:5, 6, 7, 15, 16, 17
- 特殊值:空字符串
-
因果图与决策表(Cause-Effect Graphing & Decision Tables)
- 适用场景:当输入条件之间存在复杂的组合关系时,等价类和边界值分析可能不足以发现组合缺陷。
- 因果图:一种将输入条件(原因)与输出结果(结果)之间的逻辑关系可视化的工具。
- 决策表:由因果图转化而来,清晰地列出了所有条件组合与对应动作的矩阵。
- 步骤:
- 识别原因和结果。
- 分析逻辑关系(与、或、非等),绘制因果图。
- 将因果图转换为决策表。
- 为决策表的每一列设计一个测试用例。
-
状态转换测试(State Transition Testing)
- 适用场景:适用于具有状态记忆的系统,即系统的行为不仅取决于当前输入,还取决于其历史状态。例如,ATM、在线订单系统等。
- 步骤:
- 识别系统的所有重要状态。
- 识别导致状态变化的事件(输入或操作)。
- 绘制状态转换图(State Transition Diagram)。
- 设计测试用例来覆盖所有状态、所有转换、特定转换序列等。
stateDiagram-v2 [*] --> Unauthenticated Unauthenticated --> Authenticated: Login Success Unauthenticated --> Unauthenticated: Login Fail (attempts < 3) Unauthenticated --> Locked: Login Fail (attempts = 3) Authenticated --> [*]: Logout Locked --> Unauthenticated: Unlock (e.g., by admin)
-
错误推测法(Error Guessing)
- 核心思想:基于测试人员的经验、直觉和对常见编程错误的理解来设计测试用例。
- 特点:是一种非系统性的方法,但往往能高效地发现特定类型的错误。
- 常见推测点:输入为空、输入为 0、输入包含特殊字符、大数据量、并发操作等。
-
语法测试(Syntax Testing)
- 适用场景:当输入数据必须遵循严格的格式或语法时,如命令行工具、文件解析器、API 请求等。
- 目标:验证系统能否正确接受合法格式的输入,并拒绝所有非法格式的输入,同时保证系统不会因此崩溃。
总结:黑盒与白盒的协同
白盒测试和黑盒测试并非相互排斥,而是相辅相成的。在实际的测试策略中,两者通常结合使用,以达到最佳的测试效果。
特性 | 白盒测试(White-box Testing) | 黑盒测试(Black-box Testing) |
---|---|---|
测试依据 | 程序的内部逻辑结构和代码实现 | 软件的需求规格说明书 |
视角 | 开发者视角,关注「如何实现」 | 用户视角,关注「能做什么」 |
优点 | - 测试覆盖度高,可量化 - 能发现代码深层的逻辑错误 |
- 更贴近用户实际使用场景 - 无需了解代码,测试与开发可独立 |
缺点 | - 无法发现需求规格本身的错误 - 成本高,需要编程知识 |
- 覆盖度难以衡量 - 无法测试到代码的特定分支或隐藏逻辑 |
适用阶段 | 单元测试、集成测试 | 系统测试、验收测试 |
最佳实践
一个全面的测试策略通常始于白盒测试,以确保软件的基本构件(单元)是健壮和正确的。随后,通过黑盒测试来验证整个系统的功能和行为是否符合用户需求。这种从内到外的测试过程,能够系统性地保证软件质量。
思考题
1. 简述白盒静态测试的 4 个特征、4 个功能和 4 种形式
4 个特征
- 发现问题:核心目标是检查代码和设计中的错误与遗漏。
- 遵循规则:评审过程有预设的规则,如时间、代码量和职责。
- 会前准备:参会者需提前熟悉材料,准备是评审成功的关键。
- 书面报告:会议结束后需产出正式报告,记录问题并跟踪。
4 个功能
- 沟通交流:促进团队成员间的技术学习与项目理解。
- 提升质量:通过同行压力和专家建议,提高代码和文档质量。
- 团队建设:增进成员间的相互理解和尊重。
- 解决难题:通过集体讨论为棘手问题寻找解决方案。
4 种形式(正式程度递增)
- 同行评审:非正式,开发者之间互相检查代码。
- 代码走查:较正式,由代码作者向评审小组讲解代码逻辑。
- 代码审查:最正式,作者不参与讲解,由评审组系统性地审查。
- 桌面检查:个人进行的检查,对照错误列表推演代码。
2. 简述白盒动态测试的 9 个逻辑覆盖层级和相互关系
9 个逻辑覆盖层级:
- 语句覆盖:每个可执行语句至少执行一次。
- 判定覆盖(分支覆盖):每个判定的真、假分支至少各执行一次。
- 条件覆盖:每个判定中的每个原子条件都至少取一次真值和假值。
- 判定/条件覆盖:同时满足判定覆盖和条件覆盖。
- 条件组合覆盖:每个判定中所有原子条件的可能取值组合至少执行一次。
- 修正判定/条件覆盖(MC/DC):每个原子条件都能独立地影响判定的最终结果。
- 路径覆盖:覆盖程序中所有可能的执行路径。
- 数据流覆盖:覆盖变量从定义点到使用点的路径。
- LCSAJ 覆盖:覆盖所有线性代码序列和跳转。
相互关系:
- 覆盖强度由弱到强:
路径覆盖 > 条件组合覆盖 > MC/DC > 判定/条件覆盖 > {判定覆盖, 条件覆盖} > 语句覆盖。 - 判定覆盖和条件覆盖的强度无法直接比较,它们关注点不同。
- 数据流覆盖和 LCSAJ 覆盖是从不同维度度量覆盖率,不完全属于上述逻辑覆盖的强弱层级关系中。
3. 简述黑盒测试的 6 种方法,黑盒测试的最优策略是什么?
6 种方法:
- 等价类划分:将输入数据划分为有效和无效等价类,从每类中选取代表进行测试。
- 边界值分析:重点测试输入/输出范围的边界及邻近值,是等价类划分的补充。
- 因果图/决策表:分析输入条件的组合关系及其对应的输出结果。
- 状态转换测试:针对有状态记忆的系统,测试其状态变迁的有效性和完整性。
- 错误推测法:依据经验和直觉,推测程序可能存在的缺陷并设计测试用例。
- 语法测试:针对有严格输入格式的系统,测试其对合法/非法语法的处理能力。
最优策略:没有唯一的「最优」策略,最优策略是根据被测软件的特点,将多种方法有机地结合起来,互为补充。
一个典型的组合策略是:
- 打好基础:首先使用等价类划分和边界值分析来确定核心的测试用例集。
- 考虑组合:如果输入条件之间存在复杂的相互作用,则使用因果图/决策表来处理组合情况。
- 按需选用:
- 若软件有明显的状态特征(如订单系统),则采用状态转换测试。
- 若输入有严格的格式要求(如编译器),则采用语法测试。
- 经验补充:最后,运用错误推测法来补充可能被系统方法遗漏的、基于经验的测试点。