单元测试实战

单元测试的范围可以是Unit (代码片断)、Module(模块)或者是Component(构件)

单元测试不仅仅是作为无错编码一种辅助手段在一次性的开发过程中使用,单元测试必须是可重复的,无论是在软件修改,或是移植到新的运行环境的过程中。因此,所有的测试都必须在整个软件系统的生命周期中进行维护。
经常与单元测试联系起来的另外一些开发活动包括代码走读(Code review),静态分析(Static analysis)和动态分析(Dynamic analysis)。静态分析就是对软件的源代码进行研读,查找错误或收集一些度量数据,并不需要对代码进行编译和执行。动态分析就是通过观察软件运行时的动作,来提供执行跟踪、时间分析,以及测试覆盖度方面的信息。

在客户端/服务器应用软件中,需要明确地定义协议,从而使所有客户端都使用相同的协议来与服务器进行通信。这简化了代码重用和远程数据源中提供的信息,并为智能客户功能提供了可能性。相应地,开发框架也被扩展成为客户端和服务器端两种框架。客户端框架不但提供了与桌面应用程序框架相同的功能,而且还提供了与服务器通信所需的命令代码,以及从服务器接收并自动更新客户端的代码;服务器端框架提供了从多个客户端接收并处理请求的代码,以及为了数据持久化而与数据库或远程信息提供商连接的代码等功能;同时,这些框架要能够处理有状态的事务和间断的网络连接。

客户端/服务器应用软件生命周期并没有明显的变化,版本的更新速度仍然越来越难以满足用户的需要。并且用户通常因为“懒”而不去升级客户端,使得新版本软件不得不兼容各种旧版本的软件,随着旧版本数量的增多,开发和测试工作的难度呈指数上升,以致于到某个时候不得不进行客户端强制升级,这对于用户来说显然是不够友好的

互联网应用软件的开发和测试
互联网应用软件带来了软件开发和测试的第三次进化,它在以下领域进行了扩展:
● 互联网应用软件意味着无状态。HTTP的设计就是无状态的。来自互联网应用软件的每个请求都是原子的,并且与以前的请求没有关联。而这对于系统的体系结构和数据中心是非常有好处的。
● 互联网应用软件是与平台无关的,可以在任何操作系统上开发,只要能够实现命令协议和服务器网络连接即可。
● 互联网应用软件只需要客户端提供显示功能以及执行简单脚本的能力,这点浏览器通常足以胜任,这样避免了兼容大量老版本客户端的烦恼。
● 互联网应用软件相比客户端/服务器应用软件更为集中,因此维护和部署更为简单快速。
● 由于互联网应用软件的出现,SaaS(软件即服务)成为现实,用户可以根据需要租用软件,而非一次性买下软件。
互联网应用软件对软件生命周期产生了深远的影响,形成了互联网应用软件生命周期,大致如下:
● 以网站站点物理模型来说明程序。
● 编写软件。
● 对应用程序进行单元测试。
● 修改在单元测试中发现的问题。
● 内部人员对应用程序进行测试。
● 修改测试中发现的问题。
● 将软件发布到互联网上。
● 为已经在提供服务的软件快速修复缺陷。
后面还会提到,在测试驱动开发(TDD)理论中,上面的第二步和第三步还要互换一下位置,即先编写测试,再开发应用程序代码,这听上去很有意思。
现在比较一下互联网应用软件生命周期和桌面应用软件生命周期,从中能发现什么?最显著的区别是互联网应用软件生命周期更简单,经过的步骤更少,因此互联网应用软件能解决传统软件版本更新速度慢的问题,非常快速地响应用户的需求,第一时间推出新版本的软件。对于互联网软件公司来说,新产品从最初开发到上线运营一般只需要2到3个月的时间,有些甚至只用三周的时间;产品大的版本更新的速度大约为一周到一个月;而小的维护版本(快速修复用户反馈的缺陷、个别功能的修改等等)更是可以达到每周两次常规更新。这样的版本更新速度对于传统软件是不敢想象的。
互联网应用软件生命周期简化了传统的软件生命周期,省去了其中的一些步骤;同时,互联网应用软件又具有许多传统软件所不具备的特点;那么相应地,必然要采用新的测试技术与方法,才能有效保证互联网应用软件的质量。接下来就来看看互联网应用软件的测试具有哪些特点。
仔细研究后可以发现,造成上述7点时间花费的原因可以归结为:
1.代码质量不高,存在很多缺陷。
2.系统测试发现的缺陷比较难以定位。
3.为了修复缺陷而修改代码时,很可能会不小心犯错,但是又不能及时发现这些新错误。
4.性能问题很难定位,性能优化的时间很难控制。
好了,其实到这里,解决问题的答案已经出来了,单元测试能解决全部的4个问题:
1.经过单元测试的代码,其质量能够得到保证(当然这取决于单元测试是否是有效的),大部分缺陷能够在单元测试时被发现并解决。
2.单元测试发现的缺陷很容易定位,经过有效的单元测试并修复测试所发现的缺陷后,剩下的留到系统测试阶段才被发现的缺陷已经没有几个了,并且缺陷之间的相互干扰也会变小,更容易定位。
3.即使在修改代码时不小心犯了错,通过单元测试,立刻就能发现所犯的错误,避免等到QA工程师在回归测试时才发现严重的错误。
4.性能问题同样是越早发现越容易定位,在单元测试的时候就进行性能测试,可以发现相当一部分性能问题,从而在早期就解决它们,避免了后期的定位困难,以及性能问题相互影响导致情况变得更加复杂。
另外,需要特别说明的是,在各个阶段的软件测试中,单元测试是能够达到自动化程度最高的一个,这是由各个阶段软件测试的特性所决定的。通常对于系统测试而言,自动化程度一般最多只能达到20%~30%,而单元测试用例,可以说就是完全自动执行的。这在回归测试以及代码重构后的测试时作用非常明显,高度的自动化单元测试意味着更高的测试效率、更短的测试周期、更高的可靠性。

层结构是一种严格分层方法,即数据访问层只能被业务逻辑层访问,业务逻辑层只能被表示层访问,用户通过表示层将请求传送给业务逻辑层,业务逻辑层完成相关业务规则和逻辑,并通过数据访问层访问数据库获得数据,然后按照相反的顺序依次返回将数据显示在表示层。由于三层结构将业务逻辑和数据访问分开,因此开发人员可以专注于业务逻辑,而不用关心如何去访问数据库,代码可以有效地复用,并且易于重构;通过数据访问层定义的数据访问对象(DAO),开发人员可以用自己习惯的面向对象的方式来访问数据库;将不同职能的代码划分为清晰的层次结构,使得代码的可测试性大大提高。三层结构的出现,解决了二层结构的原生性问题。

进行单元性能测试,可以发现以下性能问题。
1.方法的性能问题。软件的相当一部分性能问题是由于方法的性能不佳引起的。系统的性能瓶颈定位到方法,一般有两种表现,单次执行某个方法耗时过多,以及某个方法被调用的次数太多。
● 单次执行某个方法耗时过多。这个很容易理解,如果某个方法单次执行就需要花费几百毫秒甚至上千毫秒,那么使用该方法的功能点的性能一定好不到哪去;如果对某一功能点有很高的性能要求,那么就至少需要在单元测试通过后保证每个方法单次执行的时间都不能太长,尤其是那些被普遍使用的关键底层方法。
造成单次执行某个方法耗时过多的原因可能有:
➢ 方法中使用了效率很低的算法。
➢ 方法中执行了速度很慢的SQL语句。
➢ 方法中太多次地创建和销毁外部连接(例如数据库连接)。
➢ 方法中包含了大量的I/O。
● 某个方法被调用的次数太多。这是另一种情况,其实和第一种情况相比,最后产生的结果是一样的,就是执行某个方法的总耗时很多。这种情况往往是不良的算法导致的,需要对算法进行优化,有时可能需要在架构上做出调整。
● 对于方法的性能问题,还有一个需要关注的问题是,不同数量级、不同数据分布下不同算法的性能差异。对于算法的性能评价,一般包括时间复杂度和空间复杂度。前者基本决定了在不同数量级时算法耗时的多少,后者决定了不同数量级时算法耗费存储空间的大小。
● 关于算法的时间复杂度和空间复杂度,在各种算法分析的书籍中已有很详细的讨论,在此不做过多描述。了解了算法的复杂度之后,显然,不同的算法适合于不同的数量级和不同的数据分布(输入实例的初始状态),而通过单元性能测试,可以根据测试结果选择最适合于本系统的算法。
2.多线程并发问题。并发测试不应该等到系统测试阶段才做,那样会使得定位性能问题非常 难。在单元测试阶段就进行多线程并发测试,至少可以帮助发现两类问题,多线程并发执行时方法的性能,以及多线程并发执行时对共享资源的竞争。
● 多线程并发执行时方法的性能。就像传统的性能测试那样,可以通过并发测试得到多线程并发时方法的最大处理能力(例如A方法每秒可执行300次,B方法每秒可执行400次),以及在不同线程数时方法的平均执行时间、最大执行时间。虽然通常无法从系统预期的业务目标中分析得到方法应具备的最大处理能力、平均响应时间、最大响应时间的较为精确的值,但是这仍然有助于发现一些性能明显达不到要求的方法并优化它。
● 例如,一个即时通信系统要求每台登录服务器每秒能完成100次登录,而经过单元性能测试,多线程情况下验证用户名密码的方法最多能达到每秒执行90次,那么很明显,该方法需要被优化,即使该方法的最大处理能力刚好能达到每秒100次,这仍然不够,因为系统上的一次登录的开销要比执行一次该方法的开销大,因此至少要保证该方法每秒能执行100次以上。

测试驱动开发的基本过程包括:
1.明确当前要完成的功能。可以记录成一个TODO列表。
2.快速完成针对此功能的测试用例编写。
3.测试代码编译不通过。
4.编写对应的功能代码。
5.测试通过。
6.对代码进行重构,并保证测试通过。
7.循环完成所有功能的开发。
测试驱动开发一般遵循以下原则:
1.测试隔离。不同代码的测试应该相互隔离。对一块代码的测试只考虑此代码的测试,不要考虑其实现细节(比如它使用了其他类的边界条件)。
2.一顶帽子。开发人员开发过程中要做不同的工作,比如:编写测试代码、开发功能代码、对代码重构等。做不同的事,承担不同的角色。开发人员完成对应的工作时应该保持注意力集中在当前工作上,而不要过多的考虑其他方面的细节,保证头上只有一顶帽子。避免考虑无关细节过多,无谓地增加复杂度。
3.测试列表。需要测试的功能点很多,应该在任何阶段想添加功能需求问题时,把相关功能点加到测试列表中,然后继续手头工作。之后再完成对应的测试用例、功能代码,以及重构。这样既可以避免疏漏,也避免干扰当前进行的工作。
4.测试驱动。这个比较核心。完成某个功能,某个类时,首先编写测试代码,考虑其如何使用、如何测试,然后再对其进行设计、编码。
5.先写断言。测试代码编写时,应该首先编写对功能代码的判断用的断言语句,然后编写相应的辅助语句。
● 可测试性。功能代码设计、开发时应该具有较强的可测试性。其实遵循比较好的设计原则的代码都具备较好的测试性。比如较高的内聚性,尽量依赖于接口等。
● 及时重构。无论是功能代码还是测试代码,对于结构不合理、重复的代码等情况,在测试通过后,及时进行重构。
在测试驱动开发中,测试的范围应该如何定位?测试驱动开发强调测试并不应该是负担,而应该是帮助开发人员减轻工作量的方法。而对于何时停止编写测试用例,也是应该根据开发人员的经验,对于功能复杂、核心功能的代码,就应该编写更全面、细致的测试用例,否则只要测试流程即可。测试范围没有静态的标准,同时也应该可以随着时间而改变。对于开始时没有编写足够的测试用例的功能代码,随着缺陷的出现,根据缺陷把相关的测试用例补齐即可。

针对这些问题,这里我们整理了单元测试所需要覆盖的内容。
1.结果:确认被测单元的运行结果满足需求。
2.边界条件:找边界条件是做单元测试中最有价值的工作之一,因为缺陷经常出现在边界上。
3.路径:对基本执行路径和循环进行测试会发现大量的错误。根据白盒测试和黑盒测试用例设计方法设计测试用例。设计测试用例查找由于错误的计算、不正确的比较或不正常的控制流而导致的错误。
4.局部数据结构测试:模块的局部数据结构是最常见的错误来源,应设计测试用例以检查一下各种错误。
5.强制错误:进行各类异常测试,保证程序的健壮性。比如,内存耗光、网络不可用等情况。
6.性能特性:验证被测单元满足性能要求。

实际工作中,测试用例的设计是一个迭代的过程,它包括这样7个过程。
1.使被测单元运行
设计简单的测试用例,使用简单的输入数据,保证被测单元可以运行。
2.正向测试
设计测试用例用于验证被测单元的主干功能,而不是异常的测试用例。
3.逆向测试
设计测试用例用于验证被测单元的可靠性,保证被测单元不执行不应该完成的工作,能够处理各种异常。
4.检查需求
需要检验用例是否满足了文档中强调的所有需求,除了功能需求外,还可能有性能、安全等方面的需求。
5.验证覆盖率
应用覆盖率的设计方法,检查测试用例所达到的覆盖率。根据覆盖率的情况,补充测试用例。
6.测试执行
执行所设计的测试用例,对发现的错误进行确认、修复和回归测试。在测试过程中的动态分析可以产生代码覆盖率的报告,用以衡量测试用例是否达到指定的覆盖率目标。同时,测试过程中也有可能发现用例本身的错误,这时需要更新测试用例。
7.完善代码覆盖
随着代码的变更,可能存在错误、缺漏或冗余的用例,需要不断地完善用例。

自顶向下的增量方式可以较早发现问题,如果出现问题能够及时纠正。在测试时不需要编写驱动模块,但需要桩模块。另外,如果高层模块对下层模块依赖性很大,需要返回大量信息,在用桩模块代替时,桩模块的编写比较复杂,必然会增加开销

自底向上的增量方式可以较早地发现底层关键性模块出现的错误。在测试时不需要编写桩模块,但需要驱动模块。另外,对程序中的主要控制错误发现较晚。

有别于其他单元测试活动的是,API调用通常要求跨层、跨平台、跨语言,而一般单元测试只需要在特定的平台、框架中运行即可。API常常需要提供给其他组织、个人使用,而一般单元测试的对象只限于个人。在许多组织机构中,一般单元测试的主体是开发人员,而API测试的主体是测试人员。和其他的测试活动一样,API测试人员必须考虑系统整个业务功能,因为它是要被用户拿去使用的,这就意味着接口测试范围远远比一般单元测试要严格并且广泛,不仅要测试代码实现,还需要虑到整个接口的实际应用场景。
API的测试过程是模拟终端应用调用API的过程。在这一测试过程中,需要考虑的问题是:
1.测试用例是否覆盖了API的所有参数以及边界条件?
2.如何对API的多个参数进行组合?
3.如何初始化API调用的环境?
4.如何安排API调用的顺序以完成特定的功能?
在API测试过程中需要准备一份测试设计文档,并需要经过复审。这份文档需要结合对API设计和业务需求文档给出测试用例的设计方案。测试设计文档应该对测试工作起到指导作用,并且使你的测试更加容易。
测试用例的设计完成之后,如何组织测试代码就成了必须考虑的问题。这将直接影响你的生产力、效率,并最终影响测试的维护成本。
1.所有针对某个API的测试应该被放入一份文件中,文件的命名与API名称对应。这样其他人能够快速找到并定位所有的测试用例。当然有时候一份文件中可能会覆盖多个API。
2.在文件头部的注释中,应该包含API的声明,以便快速查阅接口的参数和返回类型。
3.每个测试用例应该相互独立。每个测试用例可以被方便地插入开发人员的单元测试框架,避免测试链的产生,即一个测试用例依赖于上一个测试用例的执行结果。
4.测试用例应该进行分组。
测试用例的执行需要参照制定的测试计划,并考虑执行的顺序以及用例筛选的策略。测试计划需要同步更新。

这道理似乎是很简单的,然而以往在单元测试时人们通常只关注被测单元的功能是否正确,很少有人会去关注被测单元是否存在性能问题。导致这一问题的一个可能的原因是,人们不清楚应该如何去测试单元的性能,也不清楚哪些性能问题是能够在单元测试时被发现的,因为对于被测单元来说,缺乏明显的性能指标。性能指标通常来源于预期的业务目标,以及所承诺的SLA。这些宏观的指标对于被测单元来说基本是没有意义的,也很难将系统的性能指标分解成单元的性能指标。
这就需要换一种角度来思考。根据性能优化的经验可以知道,系统在宏观上表现出来的性能问题,追本溯源到单元时,这个问题的表现是什么。例如系统存在内存泄漏,通常是由于单元中的对象在被使用完后,由于疏忽而被留在堆中,一般称这样的对象为游离对象。那么这就成为一个单元性能测试的性能指标,即所有被测单元在测试完成后,不能产生游离对象。通过这样的思考,可以得到一系列单元的性能指标。有了单元的性能指标,在单元测试过程中,就可以发现一部分性能问题,并以较低的成本解决。当然,单元性能测试不可能发现所有的性能问题,但它确实能有效帮助减少系统测试阶段性能优化的工作量。

何时开展单元性能测试?这个问题比较容易解决。既然是单元性能测试,肯定是在提交集成阶段之前。在传统的性能测试中,选择的时机一般是系统已经基本稳定,功能都已基本验证通过;那么在单元性能测试的时机选择上,差不多也是这个策略。要验证一个方法是否执行得足够快,是否存在多线程并发问题,是否存在内存问题等,首先要确保这个方法已经实现了正确的功能。如果这个方法实现的功能都不对,那么执行得再快,性能再好也是没用的。因此,开展单元性能测试的时机是,被测单元已经通过单元功能测试,确保被测单元已实现了正确的功能,将要被提交到集成阶段之前。这一点是很重要的,过早开展单元性能测试只是在做无用功。

另一个进行单元性能测试的理由是,它能帮助挑选适合于所开发系统的第三方组件。现在是一个开源的时代,很多时候让人们感到苦恼的不是找不到第三方组件来提供所需要的功能,而是如何从几个提供相同功能的第三方组件中挑选最适合于所开发系统的。既然功能上都能满足需求,那自然是通过比较性能来决定选用哪个。不同的组件可能在不同的数量级下会有不同的性能表现,而挑选第三方组件者要做的是,根据预期的业务目标,得到所开发系统的数量级,在此数量级下对不同的组件进行单元性能测试,找出性能最好的那个。更全面的做法是,通过测试得到不同组件在不同数量级下的性能曲线,也即了解了不同组件的性能特性,再根据系统的特点选用最合适的组件。当然,如果测试的结果是所有的第三方组件都不能满足系统的性能需求,那么就自己写一个。同样地,对于关键算法的选择也可以采用这种策略。

你可能感兴趣的:(单元测试实战)