软件构造——软件测试

本文是对软件构造课程软件测试相关内容的整理与理解。

对于测试的概念,简单来说,测试就是设计一些样例,然后运行程序,检查结果是否正确。我们将把关注的重点放在测试的目的上。另外本文还将介绍测试的种类与测试方法。
测试是确保程序正确性和健壮性的最普遍的手段。

软件测试

什么是软件测试?

软件测试,简称测试,是一种调查,目的是向测试者提供关于被测试产品或服务质量的信息。它是提高软件质量的重要手段,但也仅仅如此。它不是提高软件质量的唯一手段,也不是决定手段。因为软件的质量有很多方面,比如正确性、健壮性、可复用性和可维护性等。而软件测试,只能从正确性的角度来提高。软件测试无法软件的效率,也无法减少软件内存的占用量,也无法提高软件的可复用性和可维护性。

我们需要注意的是,即使是最好的测试,也无法保证程序 100% 没有错误。因为测试无法覆盖所有可能的输入。既然如此,我们测试的目标就不是来保证程序的正确性,而是来尽可能多地发现错误。因此,一个好的测试,应该满足以下几个方面:

  • 具有高可能性来发现错误;
  • 不冗余;
  • 是“最佳特性”;
  • 既不特别复杂,又不特别简单。

测试的层级

最底层是单元测试,往上依次为:集成测试、系统测试和验收测试。

  • 单元测试。对每一个类,或者类中的方法的测试。
  • 集成测试。对一部分类,或者包、子系统进行的测试。
  • 系统测试。对整个系统的测试。
  • 验收测试。由客户进行的测试。

还有一种测试,称为回归测试,是指在每次 bug 修复后,将以前运行过的测试用例重新运行一遍,来检查是否引入了新的 bug。回归测试一般有工具来帮助我们自动化地运行。

静态测试与动态测试

测试还可以分为两类:静态测试与动态测试。

  • 静态测试。是指不运行程序的测试,只是通过检查代码来发现问题。比如,通过用眼睛看,或者使用一些工具来辅助检查。使用静态测试,我们可以尽早地发现 bug。
  • 动态测试。是指运行程序的测试,也就是我们一般所说的测试。

测试与调试

测试与调试是有区别的:

  • 测试。是发现错误的一种方法。
  • 调试。是诊断并改正已经发现的错误的根源的一种方法。

通过测试,我们找到了错误,然后通过调试找到错误的根源,修正这个错误,然后再次测试检验错误是否被真正修复。测试与调试是相辅相成的。

白盒测试与黑盒测试

下面我们来简要介绍一下白盒测试和黑盒测试:

  • 白盒测试。是指对程序内部代码结构的测试。白盒测试关注的是程序的执行逻辑,它一般由程序员来完成。
  • 黑盒测试。是指对程序外部表现出来的行为的测试。在黑盒测试中,我们不知道程序内部是如何实现的,对程序的测试,只能通过程序提供的接口来进行。黑盒测试关注的是程序的功能,它一般由外部人员来完成。

为什么说软件测试是困难的

软件测试是困难的,可以从以下几个方面来解释:

  • 穷举测试是不可行的。
  • 随意的测试不太可能发现错误。
  • 随机或统计测试并不适合软件。也就是说基于样本的统计数据对软件测试意义不大。
  • 软件行为在可能的输入域中不连续且离散地变化。
  • 无统计分布规律可循。

对于测试,我们要转变心态,把“让其出错”和“尽快出错”作为写高质量代码的日常法宝。应当抛弃这样的想法:我的代码是宝贝,可不能总让它出错。相反,我们不仅要让它出错,还要让它尽快出错。测试的目标,就应该是让程序尽快出错,以便于尽快改正,而不应该让错误潜藏很久,造成更大的麻烦。

测试用例

一个测试用例,就是一组测试输入、执行条件和期望结果。有一些测试用例可能没有执行条件,那么它就是一个由测试输入和期望结果组成的一个序对。对于一个好的测试用例,它应该满足以下条件:

  • 最可能发现错误;
  • 不重复、不冗余;
  • 最有效;
  • 既不简单也不复杂。

测试用例必须仔细、系统地选择。

测试优先的编程与测试驱动的开发

测试优先的编程(test-first programming)是指先写程序的测试,然后再写程序的代码。使用测试优先的编程,目的就是要尽早、频繁地测试。并且,边进行开发,边测试代码会让你更加有成就感。

测试优先的编程一般有如下的流程:

  • 为函数写一个规约;
  • 再写符合规约的测试用例;
  • 写代码,然后进行测试。有问题进行改正,再进行测试。直到通过了所有的测试用例,就完成了代码的编写。

这种编程方法其实不推荐使用,因为测试优先的编程,它的目标就是通过测试用例,而这只考虑到了程序的正确性。然而程序还有许多其他的质量指标,比如,健壮性、可复用性和可维护性等,测试优先的编程忽略了它们。

测试驱动的开发(test driven development,TDD)是一个依赖于重复非常短的开发周期的开发过程:需求被转化为非常具体的测试用例,然后软件被改进以通过新的测试。

单元测试

单元测试针对软件的最小单元模型开展测试(一般来说是类和方法),隔离各个模块,容易定位错误和调试。

使用 JUnit 进行自动化单元测试

JUnit 是一个被广泛使用的 Java 单元测试框架。这个框架在 JUnit 3.8 及更早的版本中存在于 junit.framework 包中,在 JUnit 4 及之后的版本中存在于 org.junit 包中。

JUnit 单元测试是作为一个方法编写的,方法名上一行带有注释 @Test。方法通常包含对被测试模块的一个或多个调用,然后使用断言方法(如 assertEqualsassertTrueassertFalse)检查结果。如:

@Test
public void testALessThanB() {
    assertEquals(2, Math.max(1, 2));
}

黑盒测试

黑盒测试用于检查代码的功能,不关心内部实现细节。黑盒测试试图发现以下类型的错误:

  • 不正确或缺失的功能;
  • 接口错误;
  • 数据结构的错误或外部数据库访问的错误;
  • 程序行为或表现的错误;
  • 初始化和终止错误。

黑盒测试的测试用例围绕着规约和要求(也就是程序应当做什么)展开。要用尽可能少的测试用例,尽快运行,并尽可能大地发现程序的错误。

通过划分选择测试用例

等价类划分(equivalence partitioning)是一种测试方法,它将程序的输入划分为可以派生出测试用例的数据类。等价类划分的测试用例设计基于对输入条件的等价类的评估。需要针对每个输入数据需要满足的约束条件,划分等价类。等价类表示输入条件的一组有效或无效状态,每个等价类代表着对满足输入约束(有效)或违反输入约束(无效)的输入的集合。

等价类划分没有明确的标准,我们只能根据程序的行为或一些原则,选择一个我们认为比较标准的方式来划分等价类。

等价类背后的思想是将输入域划分为一些使程序具有相似行为的输入的集合,这样,从每个等价类中选一个代表作为测试用例即可。由于这样的一个测试用例可以代表这一类输入,从而就可以降低测试用例的数量。

等价类划分一般遵循如下的原则:

  • 如果一个输入条件指定了一个范围,则定义一个有效和两个无效的等价类。
  • 如果一个输入条件需要一个特定的值,则定义一个有效和一个无效的等价类。
  • 如果一个输入条件指定了集合中的一个成员,则定义了一个有效和一个无效的等价类。
  • 如果一个输入条件是布尔值,则定义一个有效和一个无效的等价类。

对于测试用例的选择,则是每个等价类的不同组合。一个测试用例,只需要从一个等价类组合的每个等价类的交集中选择一个。如果这些等价类的交集为空集,那么也就不需要从中选择一个测试用例了。

在划分中包含边界

大量的错误往往出现在输入域的边界而非中央,比如:

  • 0 是正整数和负整数的边界;
  • 数值类型的最大值和和最小值;
  • 集合(collection,这里不是指数学意义上的集合,也不是指 Java 中的 Set,而是指一组元素的有序结构,类似序集。)类型的空集(如空串、空链表、空数组等);
  • 集合(collection)的第一个和最后一个元素。

边界值分析(boundary value analysis,BVA)已经发展成为一种测试技术,它使我们在选择测试用例的时候,要倾向于考虑边界值。边界值分析是对等价类划分的补充。

覆盖划分的两个极端

选取测试用例来覆盖等价类划分有两个极端:

  • 笛卡尔积全覆盖(full cartesian product)。是指划分维度上的每一个合法组合都被一个测试用例覆盖。使用这种覆盖划分的方法,测试完备,但测试用例数量多,测试代价高。
  • 覆盖每个部分(cover each part)。每个划分维度的每个部分(即每个等价类)都被至少一个测试用例覆盖,但不一定是每个组合。使用这种覆盖划分的方法,测试用例少,代价低,但测试覆盖度未必高。

我们经常会在这两个极端之间做出妥协,这是基于人类的判断和谨慎,并受到白盒测试和代码覆盖工具的影响。

白盒测试

白盒测试(也称为玻璃盒测试,glass-box testing)意味着在了解程序功能如何实际实现的前提下选择测试用例,也就是说,白盒测试要考虑内部实现细节,根据程序执行路径设计测试用例。例如:

  • 如果内部的实现根据输入的不同而选择了不同的算法,那么你应该根据这些算法的不同划分输入域。
  • 如果内部的实现保留了一个内部缓存来记住以前输入的答案,那么你应该测试重复的输入。

白盒测试可以应用于软件测试过程的单元级、集成级和系统级。通常,它是在测试过程的早期执行的。

使用白盒测试,我们可以设计出一些测试用例,它们能够:

  • 保证模块内的所有独立路径至少执行过一次;
  • 根据逻辑表达式的真假值来走过所有的条件分支;
  • 在边界和操作范围内执行所有循环;
  • 检验内部数据结构的有效性。

有一个典型的白盒测试方法,称为“独立/基本路径测试”(independent/basis path testing)。它指对程序所有执行路径进行等价类划分,找出有代表性的最简单的路径(例如循环只需执行 1 次),设计测试用例使每一条基本路径被至少覆盖 1 次。

测试的覆盖

测试应该考虑程序内部逻辑的测试用例的代码覆盖度。代码覆盖度(code coverage)是一种度量,用来描述当一套特定的测试用例运行时,程序源代码的执行程度(通常用百分比来衡量)。一个具有高代码覆盖度的程序,在测试过程中执行了更多的源代码,这表明与低代码覆盖度的程序相比,它包含未检测到的软件错误的机会更低。有许多不同的度量可以用来计算代码覆盖度:一些最基本的是程序子例程的百分比和一套测试用例执行期间调用的程序语句的百分比。

判断一套测试用例好坏的一种方法是看它究竟是有多详细的检验了程序,这个概念就叫做覆盖(coverage)。覆盖有许多常见的种类:

  • 函数覆盖(function coverage)。程序中的每个函数都被调用了吗?
  • 语句覆盖(statement coverage)。每条语句都被某些测试用例执行了吗?
  • 分支覆盖(branch coverage)。对于程序中的每个 ifswitch-casewhilefor 语句,为真或者为假的每个方向都被一些测试用例执行了吗?
  • 条件覆盖(condition coverage)。ifswitch-casewhilefor 语句中的每个条件,它们的真假值是否都能被一些测试用例执行?
  • 路径覆盖(path coverage)。是否所有可能的分支组合——也就是程序的每条可能路径,都被一些测试用例执行了?

分支覆盖比语句覆盖更强(需要实现更多测试),路径覆盖比分支覆盖更强。也就是说,从测试效果来看:路径覆盖 > 分支覆盖 > 语句覆盖。但是分支覆盖和条件覆盖之间往往是不可比的,因为两者是相交的关系而不是包含的关系,但是存在另一种条件组合覆盖,它是要强于分支覆盖和条件覆盖的。

在行业中,100% 的语句覆盖是一个共同的目标,但是由于一些不可访问的防御代码(比如“should never get here”断言),即使是这个目标也很少能够实现。另外不幸的是,100% 的路径覆盖往往是不可行的,因为需要的测试用例数量往往是指数级的。从测试难度的角度看,仍然是:路径覆盖 > 分支覆盖 > 语句覆盖。在不同的软件类型、不同的公司中,对于达到何种标准的覆盖度都有不同的要求,在一些其安全至关重要的行业,标准更为严格。

最彻底的白盒测试方法是覆盖程序中的每一条路径,但是因为程序通常包含一个循环,所以路径的数量是很大的。执行每条路径几乎是不可能的,我们只能尽量确保覆盖度尽可能高。

EclEmma 是一个 Eclipse(一个 Java 的 IDE)的代码覆盖度工具,它可以根据测试用例来自动地计算代码覆盖度。

自动化测试与回归测试

回归测试是指一旦程序被修改,就重新执行之前的所有测试。软件工程师(从痛苦的经历中)知道,对大型或复杂程序的任何更改都是危险的。无论你是在修复另一个 bug,添加一个新特性,还是优化代码以使其更快,使用一个自动测试套件来帮助你进行回归测试可以给你带来巨大的好处。在修改代码时频繁运行测试可以防止程序在修复 bug 或添加新特性时引入其他 bug。

在程序中文档化测试策略

测试策略(testing strategy)是指根据什么来选择测试用例。测试策略非常重要,需要在程序中显式记录下来。单元测试策略也是 ADT 设计的补充文档。为了与测试优先编程的思想保持一致,往往建议根据设计测试用例的情况写下测试策略(例如划分和边界)。具体的写法是在测试类的顶部记录测试策略,然后每一个测试方法都应该在它的上方有一个注释,表明它的测试用例是如何选择的,也就是划分的哪些部分被覆盖了。

记录测试策略的目的是在代码评审过程中,其他人可以理解你的测试,并评判你的测试是否足够充分。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值