单元测试学习笔记
第一章 为什么使用单元测试1.1 程序员的工作--修改软件
修改既有代码是程序员谋生的手段。但是为什么我们需要去修改软件呢?修改软件有以下4个主要起因:
● 修正bug
● 添加新特性(feature)
● 改善设计
● 优化资源使用
只有在修正bug时,我们才需要改变软件的既有行为,而在其他情况下,我们都需要保持住软件的既有行为。如果我们在改善设计,优化,或添加新特性时改变了软件的既有行为,那我们实际上是给软件引入了bug。
可是程序员的工作就是修改软件,所以我们有很多的“机会”给软件引入bug。有什么办法能让我们的生活轻松一点,而不用因为修改代码引入bug而担惊受怕吗?
1.2 软件夹钳--测试
我们已经看到:在大多数情况下,我们希望对软件所做的改动不会改变系统的既有行为。即使是对于修正bug这种情况,我们也希望一旦bug被修正,那么修正后的正确行为能够得到保持,而不会被再之后的代码修改所改变。怎样做到这一点呢?
让我们这样想想:如果我们能在对代码进行改动之前,用一种“软件夹钳”(software vise)来固定住软件的既有行为,那么我们就可以放心大胆地去修改代码了。那么,又是什么可以来充当“软件夹钳”呢?答案是:测试。我们可以这样想一下:当一段代码被一组良好的测试所覆盖时,我们就可以放手去修改这段代码,并在修改完成之后立即运行这组测试,来验证我们的修改并没有改变既有行为而引入bug。如果确实改变了既有行为,那么测试就会明确无误地发出警报。由此可见,测试就是程序员所需要的“软件夹钳”。 1.3 单元测试与集成测试之争
我们已经知道了“测试”就是程序员所需要的“软件夹钳”。但是测试分为单元测试和集成测试,程序员需要哪种测试呢?让我们来分析一下程序员需要什么样的测试,这样或许我们就能知道程序员应该更偏向于哪种测试了。
● 程序员需要的测试应该是能帮助程序员定位错误的,这样程序员才会真正地从测试中得到“实惠”。
● 程序员需要的测试应该是很容易执行的,最好是只需点击一个按钮或键入一条命令,这些测试就能运行,而无需费时费力地去搭建测试环境及准备测试数据或仪器。
● 程序员需要的测试应该是运行速度很快的,最好能在几分钟内完成,这样程序员才能快速地得到反馈,采取下一步动作。
● 程序员需要的测试应该是易于写就的,而不愿意对代码基大动干戈,这样程序员才会愿意去写这些测试。
● 程序员需要的测试应该是自动化的,可重复的。这样程序员才会愿意重复多次地去运行这种测试。
比对程序员的需求,我们可以发现集成测试往往不能满足程序员的这些需求:
● 集成测试由于涉及多个模块,因此往往不能提供准确的错误定位信息。
● 集成测试的执行时间一般较长(小时级),这不能给程序员带来快速反馈。
● 集成测试可以自动化执行,但前提是把测试环境和测试数据等事先准备好。
● 集成测试由于不太容易写就,通常不是由程序员写就,而由专门的测试人员写就。
相反,单元测试,尤其是良好的单元测试,恰恰正是程序员所需要的那种测试:
单元测试针对单个类或单个方法,能很有成效地帮助程序员准确定位问题所在。
单元测试应该是执行时间很短的,全部执行也只需5到10分钟,程序员正好可以去喝喝咖啡。
单元测试是一种“虚拟”测试,重在测试代码逻辑,因此一般不需要真实测试环境和测试数据的支持。
单元测试是很容易写就的,尤其是有单元测试框架的帮助时。
由此可见,对于程序员而言,需要的是单元测试。程序员使用单元测试来充当软件夹钳,并在修改代码时获得快速反馈,从而更有信心地投入到修改软件的工作中。 1.4 进行单元测试的其他好处
我们已经知道了单元测试带来的一个好处:它可以充当程序员的“软件夹钳”,在程序员修改软件的过程中给予程序员快速的反馈,帮助程序员定位问题,避免引入bug。单元测试就只有这一个好处吗?不是的。下面我们就来看看引入单元测试带来的其他好处。
1.4.1 单元测试是代码的“活文档”
让文档及时反映软件设计和代码的最新情况,这是一个颇有挑战性的问题。一种较好的思路是:使用“内部”文档,即把文档同代码“拴”在一起。这样当代码发生改变的时候,文档也能相应更新。注释就是一种内部文档,良好的注释应该反映代码的最新状况。
类比来看,单元测试同样也是内部文档,因为单元测试本质上也是描述了被测类或被测方法的行为。对软件行为不了解的程序员,可以通过阅读单元测试代码来理解软件的行为。同注释相比,单元测试还具有一个更好的特性:它是一种“可执行”文档。如果单元测试在被执行时无法通过,那么说明要么单元测试没有反映当前代码的真实状况,要么说明代码中有bug。无论哪种情况,程序员都需要修改某一方,以保持两者的一致。
1.4.2 具有可测试性的软件具有更高的质量
近年来流行的极限编程方法论推崇“测试驱动开发”。我们认为,“测试驱动开发”并不一定要求必须先有测试后有代码,而关键在于要求在设计软件和实现编码时,一定要预先把软件的可测试性考虑周全。这种可测试性的重要体现就是能够方便地将单个类或方法纳入单元测试之中。具有可测试性的软件的质量往往高于不具有可测试性的软件,为什么这样说呢?
● 一个类能够被方便地纳入单元测试,往往说明这个类职责单一,也就是说它满足“单一职责原则”。
● 一个类能够被方便地纳入单元测试,往往说明它与其他类之间的耦合程度较低,相互依赖性较小,而且很可能满足“依赖抽象原则”和“开放-封闭原则”。
因此, 具有可测试性的软件,也更有可能是满足良好设计原则的软件,所以往往质量更高。 第四章 高质量的测试代码
4.1 测试代码同样要有高质量
有的开发人员认为,测试代码不是交付给最终用户的产品代码,只是内部测试使用的,因此其质量比产品代码低一点也无可厚非。我们认为这是一个错误的观点。
首先,虽然测试代码不会被交付给最终用户,但它同样是交付代码,它将被交付给下一代维护程序员,而这些维护程序员,很有可能就是我们自己。因此,为了让我们自己和其他程序员今后的生活轻松一点,我们应该在书写测试代码的时候,就以能达到的最高标准来要求。
另一方面,测试代码也必须是优质的代码。低劣的测试代码所带来的麻烦,远超过没有测试代码所带来的麻烦。因此,要么我们不写测试代码,要么就必须写出“好的”测试代码。
所以,我们希望树立起这样的观念:测试代码绝不是二等公民,必须以产品代码的标准来要求测试代码,因为测试代码也是要交付的代码。
如果有了这样的观念,那么下一个问题便是:我们怎样才能写出高质量的测试代码来?实际上,任何促使我们写出高质量的产品代码的方法和原则,也同样适用于促使我们写出高质量的测试代码。因此,只要我们遵循这些原则和方法,我们就能写了高质量的测试代码。这些方法和原则包括:
● 不留破窗原则
● DRY原则
● 单一职责原则
● 最小耦合原则
● 提高代码可读性
我们将在接下来的几节中分别来看看怎样具体贯彻这些原则和方法。
另一方面,同产品代码(production code)一样,测试代码(test <javascript:;> code)应该有自己独立的源代码树,从而方便管理,因此在本章的最后一节中,我们将介绍如何管理测试代码的源代码树。
4.2 不留破窗原则
软件中的“破窗”指的就是bug。对于测试代码而言,不留破窗原则显得尤其重要。这是因为如果在测试代码中出现了bug,这种bug是很难被检测到的。当单元测试无法通过时,程序员往往会先从被测的产品代码中去寻找bug,直到无法在产品代码中找到bug,才会想到去测试代码中碰碰运气,这是很浪费时间也破坏士气的。因此,在书写测试代码时,一定要保证第一次写就把测试代码写正确。而当发现测试代码中出现了bug时,马上修正,不留破窗!
4.3 DRY原则
DRY原则要求在软件中的每一项信息都只有单一、无歧义、权威的表示。当我们的测试代码中出现了违背DRY原则的情况时,不要犹豫,要立刻重构,去除对同一信息的重复表示。DRY原则在测试代码中还体现为代码复用。测试代码中的代码复用主要是下面的三种形式。
4.3.1 使用helper method
这是一种常见的形式,把共同代码抽取到一个helper method中,让其他测试代码调用该helper method。实际上, Test Fixture中的SetUp()和TearDown()方法就是这种形式的典型应用。
4.3.2 使用参数化测试
参数化测试常常用于对一个被测方法进行黑箱测试,即给被测方法各种不同的输入,然后检测被测方法能否对这些输入产生正确的结果。我们分别看看C++,C#和Java中怎样来进行参数化测试。 第五章 如何开发 <javascript:;>测试用例
5.1 开发测试用例的基本策略
在软件测试中,有两种不同的测试思路,它们分别是功能性测试和结构性测试。
功能性测试,也称为黑盒测试,其基本理念是:任何程序都可以被看作是将输入定义域取值映射到输出值域的函数。采用功能性测试来开发测试用例,唯一使用的信息是软件的规格说明。
结构性测试,也称为白盒测试,即测试人员将根据功能实现的方式来开发测试用例。
功能性测试的优点在于,其开发的测试用例是与软件实现无关的,即使实现发生改变,测试用例依然有效。功能性测试的缺点在于,其开发出的测试用例可能存在严重的冗余。
结构性测试的优点在于它可以提供定义良好的测试覆盖率指标,从而能够可视化地表现软件的已测试范围,而其缺点在于其开发出的测试用例依赖于实现。
因此,我们开发测试用例的基本策略是:采用功能性测试的方法来开发测试用例,然后利用测试覆盖率指标来提高测试覆盖范围和去除冗余的测试用例,从而保证测试用例的质量。
上述的测试用例的开发策略体现了这样一种思想:单元测试所测试的是类或对象的行为,而不是类或对象的成员函数;单元测试应该以行为为中心,而不必担心是哪个类在被测试。有些成员函数只是参与到一个特定的功能(feature)中,而不是实现该功能,因此不值得单独测试这样的成员函数。将重点放在测试行为上,而不是每个单独的成员函数上,我们就可以更好地兼顾到测试的覆盖率和重构的简易度。
5.2 边界值测试
边界值测试是基于如下的两个假定:
软件的bug更可能出现在输入变量的极值附近。
软件失效极少由两个(或更多)缺陷同时发生所引起。
基于以上的假定,边界值测试按如下的方法产生测试用例:
对每个输入变量,在其最小值、略高于最小值、略低于最大值和最大值处取值,这4个值被记为min,min+,max-和max。
只让一个变量取以上的极值,而其他变量都取出正常值。
边界值测试很适合于输入变量是互相独立的物理量的情况。这里的关键词是“独立”和“物理量”。由于这些变量互相独立,因此我们可以仅让其中一个变量取极值,而让其他变量取正常值。由于这些变量是物理量,因此它们往往存在极值(即使不存在极值,我们常常也可以人为设定出用于测试的极值)。
5.3 健壮性测试
健壮性测试负责测试被测方法对异常情况的处理是否正确。在单元测试中,我们应该通过模拟异常情况的发生,来测试错误处理逻辑。
但是必须注意的是,如果被测方法本身并没有被要求必须处理某类异常情况,那么就无需对被测方法进行健壮性测试。仅对真正需要进行异常情况处理的代码作健壮性测试。
5.4 等价类测试
等价类测试的思想是通过每个等价类中的一个元素来开发测试用例,从而降低测试用例的冗余度。等价类测试的关键是确定出类的等价关系,通常的方法是预测可能的实现,并考虑在实现中必须提供的功能操作。
等价类测试不仅可以针对输入变量划分等价类,也可以针对输出结果划分等价类。因此,在使用等价类测试时,要兼顾输入和输出,通盘考虑,这样往往会有很好的测试效果。当划分出合理的等价类之后,在等价类的边界值处进行边界值测试,将带来更好的测试效果。
一些常见的划分等价类的标准有:
● 存在与否
● 取值范围
● 元素顺序
● 元素个数
● 数据格式 哇 很受用~谢谢楼主分享~:victory: 谢谢楼主分享~
页:
[1]