软件测试与测试优先的编程
一、软件测试
再好的测试也无法证明系统里不存在错误。
一个好的测试应该包括以下特性
(1)可以发现错误
(2)不冗余
(3)最佳特性
(4)别太复杂也别太简单
测试的级别:
(1)单元测试:指验证特定代码部分的功能的测试,通常是在功能级别上
(2)集成测试:由多个程序员或是一个编程团队创建的两个或多个类、包、组件、子系统的组合执行。
(3)系统测试:测试一个完全集成的系统,以验证该系统是否满足要求,从而在其最终配置中执行软件。
静态测试和动态测试:
(1)静态测试:静态测试是在不实际执行程序的情况下执行的。静态测试通常是隐式的,如校对,以及当编程工具/文本编辑器检查源代码结构或编译器(预编译器)时,检查语法和数据流作为静态程序分析。
(2)动态测试:动态测试描述了代码的动态行为的测试,它实际上用一组给定的测试用例执行已编程的代码。典型的技术是使用存根/驱动程序或从调试器环境中执行
测试和调试(debug)
测试:发现是否存在错误。
调试:识别错误根源,消除错误。
白盒测试和黑盒测试:
白盒测试:对程序内部代码结构的测试。
黑盒测试:对程序外部表现出来的行为的测试。
为什么软件测试困难?
(1)对于软件测试来说,穷举+暴力=不可能,而且靠偶然测试没意义。基于样本的统计数据对软件测试也意义不大—软件与产品的巨大差异
(2) 软件行为在离散输入空间中差异巨大。例如一个系统似乎可以在广泛的输入范围内正常工作,然后突然在一个边界点失效(大多数正确,少数点错误),bug出现往往不符合特定概率分布。
(3)故障常常无统计分布规律遵循
二、测试案例
测试用例:输入+执行条件+期望结果
测试用例可以简单地是您对程序提出的一个问题。运行测试的目的是获取信息,例如程序是否通过测试。
三、测试优先的编程
需要在编写程序之前先考虑测试。不要把测试留到最后,当你有一大堆未验证的测试时代码,将测试留到最后只会使调试时间更长,更痛苦的是,因为错误可能在代码的任何地方。
步骤:
(1)为这个函数写一个规格说明(先写spec)
(2)再写符合spec的测试用例
(3)写代码、执行测试、有问题再改、再执行测试用例,直到通过它
Spec:描述了函数的输入和输出行为。比如:(1)它给出了形参的类型和对它们的任何附加约束(例如sqrt()的形参必须是非负的);(2)它还给出了返回值的类型以及返回值与输入之间的关系等等。
总之,写测试用例,就是理解、修正、完善spec设计的过程,并且,先写测试会节省大量的时间
四、单元测试
单元测试针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试。当一个模块的单元测试失败时,您可以更确信错误是在该模块中发现的,而不是在程序的任何地方。
因为组件不是一个独立的程序,所以驱动程序、存根程序必须经常为每个单元测试而开发。
驱动程序:可以说是一个“主程序”,它接受测试用例数据,将这些数据传递给组件(待测试),并打印相关的结果。
存根程序:用于替换从属于被测试组件(被调用)的模块。存根程序使用下级模块的接口,可以进行最小的数据操作,打印输入验证,并将控制返回给正在测试的模块。
五、使用JUnit进行自动的单元测试
1、JUnit介绍
JUnit是广泛采用的Java单元测试框架。JUnit在测试驱动开发的开发中一直很重要,它是被统称为xUnit的单元测试框架家族之一。2013年对GitHub上的10,000个Java项目进行的一项调查发现,JUnit是最常见的外部库,每个库被30.7%的项目使用。
2、Junit 测试
JUnit单元测试一般前面会有个@Test的字样。
单元测试方法通常包含一个或多个对被测试模块的调用,然后使用assertEquals、assertTrue和assertFalse等断言方法检查结果。例如:
在现有项目中创建一个新的JUnit测试用例或测试套件的步骤:
一个JUnit4测试例子:
六、黑盒测试
黑盒测试:用于检查代码的功能,不关心内部实现细节。
黑盒测试一般是试图找到以下类型的错误:
(1)不正确或缺失的函数
(2)接口错误
(3)数据结构或外部数据库访问错误
(4)行为或性能错误
(5)初始化和终止错误
黑盒测试的测试用例用于检查程序是否符合规约,用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误
1、通过分区选择测试用例
(1)等价类划分
基于等价类划分的测试:将被测函数的输入域划分为等价类,从等价类中导出测试用例。针对每个输入数据需要满足的约束条件,划分等价类
如果一组对象可以通过对称的、传递的和自反的关系连接起来,那么等价类就存在了。每个等价类代表着对输入约束加以满足/违反的有效/无效数据的集合。通常,输入条件可以是一个特定的数值、一个值范围、一组相关值或一个布尔条件。
等价类划分的准则:
(1)输入数据限定了数值范围,则定义一个有效的和两个无效的等价类。
(2)输入数据指明了特定的值,则定义一个有效和一个无效的等价类。
(3)输入数据确定了一组数值,则定义一个有效和一个无效的等价类。
(4)输入数据是Y/N,则定义一个有效和一个无效的等价类。
等价类划分例子:
2、在分区中保含边界
其实大量的错误发生在输入域的“边界”而非中央。
边界的例子包括:
(1)0是正数和负数之间的界限
(2)数值类型(如int和double)的最大值和最小值
(3)集合类型为空(空字符串、空列表、空数组)
(4)集合的第一个和最后一个元素
边界值分析方法是对等价类划分方法的一个补充。
发生边界错误的原因:
(1)程序员经常犯一些大小差1的错误
(2)某些边界值是“特殊情况”,需要特殊处理
(3)程序的行为在边界的地方可能发生“突变“,例如,当一个int变量增长超过其最大正值时,它就突然变成一个负数。
覆盖所有分类情况的两个极端方式:
(1)笛卡尔积:全覆盖。多个划分维度上的多个取值,要组合起来,每个组合都要有一个用例。测试完备,但用例数量多,测试代价高
(2)覆盖每个取值:最少1次即可。每个维度的每个取值至少被1个测试用例覆盖一次即可。测试用例少,代价低,但测试覆盖度未必高。
七、白盒测试
白盒测试要考虑内部实现细节,根据程序执行路径设计测试用例,白盒测试一般较早执行
独立/基本路径测试:对程序所有执行路径进行等价类划分,找出有代表性的最简单的路径(例如循环只需执行1次),设计测试用例使每一条基本路径被至少覆盖1次。
八、覆盖率的测试
代码覆盖度:已有的测试用例有多大程度覆盖了被测程序。
代码覆盖度越低,测试越不充分,但要做到很高的代码覆盖度,需要更多的测试用例,测试代价高。
可以使用许多不同的度量来计算代码覆盖率;其中一些最基本的是程序子例程的百分比和在测试套件执行期间调用的程序语句的百分比。
覆盖的种类有:函数覆盖、语句覆盖、分支覆盖、条件覆盖、路径覆盖
测试效果:路径覆盖>分支覆盖>语句覆盖
测试难度:路径覆盖>分支覆盖>语句覆盖
最彻底的白盒方法是覆盖程序中的每个路径,但由于程序通常包含一个循环,所以路径的数量很大。例如:一个程序包含一个需要执行20次的循环。它包含520个不同的执行路径。假设测试每条路径需要1 ms,那么完成所有路径的测试需要3170年。
实际当中,根据预先设定的覆盖度标准,逐步增加测试用例的数量,直到覆盖度达到标准(例如语句覆盖100%、路径覆盖90%)。
九、自动化测试和回归测试
手工测试的代价太高,最好达到完全的自动化。自动调用被测函数、自动判定测试结果、自动计算覆盖度等等。
一个好的测试框架,比如JUnit,可以帮助您构建自动化测试套件。
要注意,这里说的只是“测试用例的自动执行”,并非“自动生成测试用例”。
回归测试:一旦程序被修改,重新执行之前的所有测试。
自动化回归测试是现代软件工程的最佳实践。
十、记录你的测试策略
测试策略(根据什么来选择测试用例)非常重要,需要在程序中显式记录下来。
目的:在代码评审过程中,其他人可以理解你的测试,并评判你的测试是否足够充分。
例子:
测试是指在代码中发现bug,而测试优先编程是指在引入bug后尽早发现它们。