HIT-SC-Chapter Two
目录
HIT-SC-Chapter Two
1 Software Testing
(1) What is testing?
- 是为向涉众提供有关被测产品或服务的信息而进行的调查。
- 提高软件质量的重要手段
- 确认是否达到可用级别(用户需求)
- 关注系统的某一侧面的质量特性
- 即使最好的测试,也无法达到100%的无错误
- 残留缺陷率(residual defect rates):
- $bugs (left over) per/ kloc(1000 Lines of Code) $
- 1-10:Typical industry software.
- 0.1-1:高质量的验证。Java库可能达到这种级别的正确性。
- 0.01-0.1:The very best, safety-critical validation. NASA and companies like Praxis can achieve this level.
- 如果为行业软件,则bug的数量触目惊心
(2) Test Characteristics
- 测试的目标与其他开发活动的目标背道而驰。目标是:破坏、证错、“负能量”
- 再好的测试也无法证明系统里不存在错误
- What is a good test?
- 发现错误的可能性高
- 不冗余
- 最佳特性
- 别太复杂也别太简单
(3) Testing levels
- Unit testing:单元测试
- 指验证特定代码段功能的测试,通常在功能级。
- Integration testing:集成测试
- 由多个程序员或编程团队创建的两个或多个类、包、组件、子系统的组合执行。
- System testing:系统测试
- 测试一个完全集成的系统,以验证该系统符合其需求,从而在其最终配置中执行该软件。
- 验收测试:
- 回归测试:
- Other testing types:
(4) Static vs. Dynamic testing
- Static testing
- is performed without actually executing programs.
- 静态测试通常是隐式的,比如校对,还有编程工具/文本编辑器检查源代码结构,或者编译器(预编译器)检查语法和数据流,比如静态程序分析。
- Reviews, walkthroughs, or inspections are referred to as static testing
- Dynamic testing
- 描述代码动态行为的测试,它实际地使用给定的测试用例集执行已编程的代码。
- 动态测试可以在程序100%完成之前开始,以测试代码的特定部分,并应用于离散的函数或模块。
- 典型的技术是使用存根/驱动程序或从调试器环境执行。
(5) Testing vs. Debugging
- Testing: 发现是否存在错误
- Debugging: 识别错误根源,消除错误
- Debugging is discussed in detail in Chapter 6.4.
(6) White-box vs. Black-box testing
- White-box testing
- 通过查看源代码来测试程序的内部结构或工作方式。
- 测试程序内部代码结构
- Black-box testing
- 将软件视为“黑盒”,在不了解内部实现、不查看源代码的情况下检查功能。
- 测试其外部表现出来的功能和行为
(7) Why Software Testing is Hard
- 穷举+暴力=不可能
- 随意偶然的测试无意义
- 随机或统计测试并不适合软件。
- 基于样本的统计数据对软件测试意义不大。
- 软件在离散输入空间差异巨大
- 无统计发布规律可循
2 Test Case
- 测试用例=测试输入+执行条件+期望结果
- 可能只是您对程序提出的一个问题。运行测试的目的是获取信息,例如程序是否通过测试。
- 为一个特定的目标而开发的,例如执行一个特定的程序路径,或者验证是否符合一个特定的需求。
- 测试用例是质量保证的基石,而测试用例是用来验证产品的质量和行为的。
- good test case’s Characteristics
- 最容易发现错误
- 不重复、不冗余
- 最有效
- 既不简单也不复杂
3 Test-First Programming
- Write the tests before you write the code
- Motivation
- 尽早并经常进行测试
- 不要把测试留到最后,当您有一大堆未验证的代码时。将测试留到最后只会使调试变得更长更痛苦,因为错误可能存在于代码的任何地方。
- 开发代码时测试代码要愉快得多
- 过程:
- Write a specification for the function(spec first)
- Write tests that exercise the specification. (再根据spec写符合spec的测试用例)
- Write the actual code. Once your code passes the tests you wrote, you’re
done.(写代码、执行测试、有问题再改、再执行测试用例、直到通过它)
- 规范(spec)描述了函数的输入和输出行为。
- 函数的描述是对函数行为的描述:•参数的类型•返回值的类型•约束和它们之间的关系。
- 它给出了形参的类型和对它们的任何附加约束(例如sqrt()的形参必须是非负的)。
- 它还给出返回值的类型以及返回值与输入的关系。
- 在代码中,规范由方法签名和上面描述其功能的注释组成。
- 写测试用例,就是理解、修正和完善spec设计的过程
- 规范也可能有缺陷——不正确、不完整、不明确、遗漏了极端情况。
- 尝试编写测试可以在您浪费时间编写bug规范的实现之前尽早发现这些问题。
- 优点:
- 会节省大量的调试时间
- TDD(Test-driven development)
- 是一种依赖于非常短的开发周期的重复开发过程:将需求转化为非常具体的测试用例,然后对软件进行改进以通过新的测试,仅此而已。
- 反对允许添加未被证明满足需求的软件开发。
4 Unit Testing
- 针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试。
- 单独测试模块会使调试更容易。
- 当一个模块的单元测试失败时,您可以更加确信错误是在该模块中找到的,而不是在程序中的任何地方。
(1)procedures
- 单元测试通常被认为是编码步骤的补充。
- 每个测试用例都应该与一组预期结果相结合。
- 因为组件不是独立的程序,驱动程序和/或存根软件通常必须为每个单元测试开发。
- 驱动:接受测试用例数据的“主程序”,将这些数据传递给组件(待测试),并打印相关的结果。
- stubs:替换隶属于(由)被测试组件调用的模块。
5 Automated Unit Testing with JUnit
- 一个流行的单元测试框架:JUnit
- 很早就引进了TDD方式。并为其提供了开发包Junit。
- JUnit在编译时链接为JAR;该框架位于JUnit 3.8和更早版本的JUnit .framework包下,以及JUnit 4及以后的package org.junit。
(1)Junit test case
-
JUnit单元测试以@Test注释之前按方法形式编写。
-
单元测试方法通常包含对被测试模块的一个或多个调用,然后使用assertion(断言) methods
assertEquals, assertTrue, assertFalse
检查结果 -
例如,我们为Math.max()选择的测试在为JUnit实现时可能是这样的:
-
在现有项目中创建一个新的JUnit测试用例或测试套件
- setUp()或@Before:准备测试,完成初始化;
- tearDown()或@After:清理测试环境
(2) Example
- 测试private method…
(3) JUnit Test organization
- 为每个公共类Foo创建一个测试类FooTest
- 有一个源目录和一个测试目录
- 将FooTest和Foo存储在同一个包中
- 测试可以访问具有默认(包)可见性的成员
6 Black-box Testing
-
检查应用的功能--不观察内部结构及实现细节
-
黑盒测试试图找到以下类型的错误:
- 不正确或缺失的功能/函数
- 接口错误
- 数据结构或外部数据库访问中的错误
- 行为或性能错误
- 初始化和终止错误
-
-
-
Test cases for block-box testing
-
黑盒测试的测试用例是围绕规约和需求构建的--应用程序应该做什么/程序是否符合规约
- 测试用例通常来自软件的外部描述
- specs
- requirements
- design parameters
- 选择一组测试用例,小到足以快速运行,但大到足以验证程序。
- 尽可能少的测试用例
- 尽快运行,尽快发现可能大的程序错误
- 测试用例通常来自软件的外部描述
-
6.1 Choosing Test Cases by Partitioning
(1) Equivalence Partitioning 等价类划分
-
将程序的输入域划分为可以从中派生出测试用例的数据类。
- 基于等价类划分的测试:将被测函数的输入域划分为等价类,从等价类中导出测试用例
-
针对每个输入数据需要满足的约束条件,划分等价类
- 如果一组对象可以通过对称、传递和自反的关系链接,则存在等价类。(
集合论…)
- 如果一组对象可以通过对称、传递和自反的关系链接,则存在等价类。(
-
等价类表示输入条件的一组有效或无效状态。
- 每个等价类代表着对输入约束加以满足/违反的有效/无效数据的集合
- 通常,输入条件可以是特定的数值、一组值、一组相关值或布尔条件。
-
等价类背后的思想是将输入空间划分为程序具有类似行为的类似输入集,然后使用每个输入集的一个代表。
- 降低测试用例数量
-
等价类划分指南(根据输入值):
-
数值范围
-
特定的值
-
确定的一组数值
-
布尔值
-
……
-
例:
为了生成测试套件,我们将从网格的每个正方形中选择任意一对(a,b)
这些点是我们可以选择完全覆盖分区的测试用例。
-
6.2 Include Boundaries in the Partition
-
更多的错误发生在输入域的边界,而不是在“中心”
- 0是正数和负数之间的边界(边界不一定在边缘)
- 数值类型的最大值和最小值,如int和double
- 集合类型的空(空字符串、空列表、空数组)
- 集合的第一个和最后一个元素
-
BVA(Boundary Value Analysis)已开发为一种测试技术,导致对边界值的测试用例的选择。
- 一种补充等效划分的测试用例设计技术。
- 不是选择等价类的任何元素,BVA而是在类的“边缘”选择测试用例
-
边界值分析方法是对等价类划分方法的补充
-
为什么bug经常发生在边界上?
- 程序员经常会犯一个错误(比如写<=而不是<,或者将计数器初始化为0而不是1)。
- 一些边界可能需要作为代码中的特殊情况来处理。
- 边界可能是代码行为中不连续的地方。例如,当int变量增长超过其最大正值时,它会突然变成负数。
- null算什么?…
- 将边界作为子域包含在您的分区中是很重要的,这样您就可以从边界选择输入。
- 在等价类划分时,将边界作为等价类之一加入考虑
-
边界值分析指南
- 结合等价类与边界值分析
-
例:
-
覆盖等价类的两个极端
- 笛卡尔积:全覆盖
- 多个划分维度的多个取值,要组合起来,每个组合都要有一个用例
- 对于包含边界的max示例,它有三个维度,分别由3个部分、5个部分和5个部分组成,它的意思是最高需要$355=75$
- 测试完备、用例数量多、测试代价高
- 覆盖每个取值:最少一次即可
- 每个维度的每个取值至少被一个测试用例覆盖一次即可
- 如果仔细选择,max的测试套件可以小到5个测试用例。
- 测试用例少、代价低、测试覆盖度未必高
- 我们经常处于这两个极端之间,基于人类的判断和谨慎,并受到白盒测试和代码覆盖工具的影响。
- 笛卡尔积:全覆盖
7 White-box Testing
-
黑盒$VS.$白盒
- 黑盒测试完全从spec中导出测试用例,不考虑函数内部实现
- 我们划分并寻找multiply和max的边界,而没有查看这些函数的实际代码。
- 白盒测试要考虑内部实现细节
- 例如,如果函数实现根据输入选择不同的算法,那么您应该根据这些域进行划分。
- 如果实现保留了一个内部缓存来记住以前输入的答案,那么您应该测试重复的输入。
- 黑盒测试完全从spec中导出测试用例,不考虑函数内部实现
-
利用系统内部视角和编程技巧设计测试用例
- 根据程序执行路径设计测试用例
- 白盒测试可以应用于软件测试过程的单元级、集成级和系统级。通常,它会在测试过程的早期执行。(一般较早实现)
- 聚焦于代码实现
-
使用白盒测试方法,您可以派生出测试用例
- 保证一个模块内的所有独立路径都至少执行过一次
- 测试所有逻辑判断的正确与错误路径
- 在所有循环的范围内执行其边界
- 使用内部数据结构以确保其有效性。
-
典型的白盒测试方法称为“独立/基路径测试”
- 对程序所有执行路径进行等价类划分,找出有代表性的最简单路径(如循环只需执行一次),设计测试用例使每一天基本路径至少被覆盖一次
8 Coverage of Testing
- 测试应该考虑程序内部逻辑的测试用例的代码覆盖度。
- 代码覆盖率是用来描述在特定测试套件运行时程序源代码执行的程度的度量。
- 多大程度上覆盖了测试程序
- 通常用百分比衡量覆盖度
- 最基本的是程序子例程的百分比和测试套件执行期间调用的程序语句的百分比。
- 代码覆盖度越低,测试越不充分;越高,测试代价越高
- coverage
- 判断测试套件的一种方法是询问它对程序的执行有多彻底。
- kinds:
- 函数覆盖
- 语句覆盖
- 分支覆盖(if,while)
- 条件覆盖
- 路径覆盖
- 测试效果:$路径覆盖\textgreater 分支覆盖\textgreater 语句覆盖$
- 100%$语句覆盖$在行业内是一个共同的目标,但由于不可到达的防御代码(比如“永远不应该到这里”的断言),即使是这个目标也很少实现。
- $100%分支覆盖$是非常可取的,而安全关键行业的规范甚至有更艰巨的标准。
- $100%路径覆盖$是不可行的,需要指数级大小的测试套件来实现。
- 测试难度:$路径覆盖\textgreater 分支覆盖\textgreater 语句覆盖$
- 最彻底的白盒方法是覆盖程序中的每个路径
- 但路径数量巨大,难以覆盖
- 执行每一个路径几乎是不可能的,我们只能尽量确保覆盖率尽可能高。
- 一种标准的测试方法是:
- 添加测试直到测试套件达到足够的覆盖:也就是说,程序中每个可到达的语句都被至少一个测试用例执行。
- 在实践中,覆盖率通常是通过代码覆盖率工具来测量的,它计算了测试套件运行每条语句的次数。
- 有了这样的工具,白盒测试就很容易了;您只需测量黑盒测试的覆盖率,并添加更多的测试用例,直到所有重要的语句都被记录为已执行。
- EclEmma:
- 测试套件已经执行的代码行被标为绿色,未被覆盖的代码行被标为红色。
- 下一步是提出一个导致红线执行的测试用例,并将其添加到您的测试套件中,以便红线变成绿色。
9 自动化测试和回归测试
(1)Automated testing
- 手工测试代价太高,最好达到完全的自动化
- 自动调用被测函数、自动判定测试结果、自动计算覆盖度
- 测试驱动程序不应该是一个交互程序,它提示您输入并打印出结果供您手动检查。
- 相反,测试驱动程序应该在固定的测试用例上调用模块本身,并自动检查结果是否正确。
- 测试驱动程序的结果应该是“所有测试OK”或“这些测试失败:…”
- 一个好的测试框架,可以帮助您构建自动化测试套件。
(2) 自动化测试vs.自动化测试生成
- 请注意,像JUnit这样的自动化测试框架使运行测试变得很容易,但是您仍然需要自己提出好的测试用例。
- 只是测试用例的自动执行,并非自动生成测试用例
- 自动测试用例生成还太难
- 至今仍是计算机科学研究的一个主题。
(3) Regression testing 回归测试
- 回归测试:
- 一旦实现了测试自动化,在修改代码时重新运行测试是非常重要的。
- 即一旦程序被修改,需要重新执行之前的所有测试。
- 在修改代码时频繁运行测试可以防止程序退化——在修复新bug或添加新特性时引入其他bug
- 在每次更改后运行所有测试称为回归测试。
- 需要:
- 当您发现并修复bug时,将引发bug的输入添加到您的自动化测试套件中作为测试用例。
- 这种测试用例称为回归测试。这有助于用好的测试用例填充测试套件。
- 每个回归测试都在一个版本的代码中完成!
- 保存回归测试还可以防止重新引入bug的回归。
(4) Automated regression testing
- This idea also leads to test-first debugging.
- 当bug出现时,立即为它编写一个测试用例,并立即将其添加到测试套件中。
- 一旦您找到并修复了bug,您的所有测试用例都将通过,并且您将完成调试并对该bug进行回归测试。
- 在实践中,自动测试和回归测试几乎总是结合使用的。
- 回归测试只有在测试可以经常自动运行的情况下才实用。
- 相反,如果您已经为您的项目进行了自动化测试,那么您可以使用它来防止回归。
- 自动化回归测试是现代软件工程的最佳实践。
持续集成
10 Documenting Your Testing Strategy
-
单元测试策略是ADT设计的补充文档。
-
Test Strategy:
- 根据什么选择测试用例
- 与测试优先编程的思想一致,建议根据您设计测试用例的情况写下测试策略(例如分区和边界)。
- 目的使使代码评审时,其他人可以理解你的测试,并批判你的测试是否足够充分
-
example:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理