软件构造心得(二)
本次随笔继续讲述关于软件测试的那些事。
软件测试(二)
软件测试有几种分类方式。
按照测试方式(查错方式),可以分为静态测试和动态测试。静态测试就是盯着代码看,不通过任何实操通过双眼和大脑分析代码,找出逻辑错误(俗称盯代码);而动态测试就是通过输入各种测试用例来找到错误,再从实际的样例来发现代码中错误的地方以此进行修改。
而一般在debug的时候,我们更常用的是动态测试(马原:认识必须统一于实践)。而动态测试就是造出各种测试样例来测试软件是否出错,这之中肯定没法穷举,随便编造又没有代表性。因此下面会介绍不同动态测试类型下的几种编造测试样例的方法。
一、黑盒测试:
黑盒测试,顾名思义就是不考虑中间程序实现的过程,只要看输出结果是否正确即可。
1、等价类划分方法:
等价类划分方法就是把数据范围划分为几个等价类,然后在这些等价类中分别造一或若干组数据,用这些数据进行测试。看起来很简单,但是其中会有一些问题:
(1)究竟要分多少组?是不是整个范围划成一组也行?是不是分的组越多越好?
(2)每个等价类之中要怎么造数据?完全随机可以吗?
显然,对于第一个问题,没有很硬性的规定。但是显然,把整个范围划成一组显然是不行的。因为这样划了等于没划,就相当于全范围完全随机(因为等价类划分的原理就是减少重复测试到同一种或比较类似的样例的次数)。一般来说,分的组数越多,测试的效果越好(起码不会变差),但是由于随着等价类数量变多,测试的数据集越大,测试的时间成本和压力就越大(等价类趋于无限时就成了穷举)。分组的时候可以按照大数据和小数据分类,也可以按照对象本身特点分类(如图中可分为连通图、树、带环图/树形图、DAG等)。但最理想的情况是等价类能够刚好覆盖代码中出现的所有情况,这就跟代码本身的结构相关,那就不是黑盒测试了,在后面白盒测试中会有所提及。
对于第二个问题,首先随机造数据也不是不行,确实是一种方法(如算法竞赛中会出现的对拍)。但是,可以与下面第二种方法相结合,不要全取范围中间的数据,可以也取几个临界数据。
2、特殊值法(临界取值法):
这种方法相对于第一种方法就显得有些玄学(雾),就是多取一些特殊值的数据进行测试(如分类的临界值、0点、图中的平凡图和空图和自环等)。这个方法的原理是在临界值中不知道是使用两种分类的方法中的哪一种更合适(包括特殊点(零点)),因为以函数图像来看,它的结果可能不是连续的,也许是左连续、右连续甚至是跳跃的。因此你在编写程序的时候可能会漏掉这种类似的特殊情况从而使软件不能合理处理这种情况。(这种方法在算法竞赛中也经常使用,看看是否是特殊数据的锅)
二、白盒测试:
白盒测试,与黑盒测试相对,就是在测试的过程中考虑程序的结构和实现过程,从而来构造数据(或判断构造数据的优劣)。
使用的方法也仍能用以上的两种方法,但是对于等价类划分和临界值选取就要更要按照程序本身的结构和逻辑,尽可能取到最优(即在尽可能少的数据集中覆盖代码中所有情况的测试)。
下面举一个简单的例子:
if (score<0||score>100) printf("Error Score"); else if (score>=90) printf("A"); else if (score>=70) printf("B"); else if (score>=50) printf("C"); else printf("D");
这是一个简单的根据成绩判断等级的选择语句程序(数据范围为0到100)。在黑盒测试中,由于不考虑代码内部结构,因此划分等价类可能会划为{[0,20], (20,40], (40-60], (60,100]},这显然不是一个很优秀划分方法,因为我们看程序实现,显然一个等价类中的数据可能会归属于不同的情况。那么根据程序的实现,我们就可以划分出与程序最贴合的等价类:{[0,50), [50,70), [70,90), [90,100]}。这就是白盒测试中构造数据的原理(即遍历每一种情况)。
但是,当代码中出现循环和选择语句组合及其嵌套时,此时想要手动完美划分等价类就显得有点困难。这里引入代码覆盖度的概念,代码覆盖度就是评估测试数据集覆盖了多少行代码/多少语句/多少情况(覆盖即为测试时执行到了)。其中最重要的还是路径覆盖。何为路径覆盖?这里可以引入流程图来看,如下流程图。每一个样例从开始到结束的走过的点和边组成一条路径,路径全覆盖就是指你的数据集遍历了流程图的每一个点和每一条边,此时显然我们对该程序的测试是最为充分的。
代码覆盖度可以很好地体现等价类划分的质量,在一些很复杂的程序中,可以先凭借感觉(大致数据)构造出测试数据集,然后通过专门的插件(EclEmma)判断代码覆盖度,以此判断出自己的数据集是否相对合理,然后再对数据集进行增补和修改。
说完了动态测试的各种方法,来介绍一下测试的合适流程与注意事项。首先,必须要说的是,测试必须边编写代码边测试!!!每写完一小部分就对每一个单元进行测试,早发现错误早修改,化整为零才能降低代码debug的难度。
下面简单说一下软件构造与测试间的先后关系(流程):
1、编写测试规约(写出每个方法测试的等价类、数据构造的思路);
2、编写测试代码;
3、编写代码(code)并测试;
其中,我一开始对于先写测试代码再写代码这一步骤感觉有点疑惑,难道测试的目的不就是为了验证代码的正确性吗?为什么要先写测试代码呢?
一方面,先写测试代码相当于先写出了程序的预期,先从使用者的角度看待代码,使得在编码时能够比较契合用户需求。同时,先写测试代码也是体现了等价类的情况,这从某种意义上也是一种代码结构的体现,根据已经写好的测试代码来调整本身代码的实现,时刻保证了不偏离主题、不破坏预期结构。
另一方面,它保证了编码时的连贯性,不会边写代码边加测试用例以此破坏写代码时的连贯性;它也能够保证在编码的每时每刻都能够进行测试,做到了真正把问题分解成具体的多个小问题。而随着编码的进行和测试的进行,也能够很好地看到自己编码的进度。