1.4 单元测试的发展趋势

前面已经介绍过单元测试的 天和今天,现在来看看单元测试的明天。这么说可能并不准确,因为下面要讲的单元测试发展趋势,有些已经在推广实践了。尽管如此,这些新的单元测试理论、方法、技术仍然在发展中,正在变得越来越成熟、越来越完善。而所有这些所展示出来的美好前景,都是令人无比 动的。

1.4.1 单元性能测试

传统的单元测试仅仅针对被测单元的功能进行测试,而没有考虑到被测单元的性能。然而对于互联网应用软件来说,高性能是一个基本的、非常重要的要求,同时互联网应用软件生命周期很短,因此,互联网应用软件的性能测试对于QA工程师来说,是一个很大的挑战。如何才能在最短的时间内发现性能问题、定位性能问题、解决性能问题?尤其是定位和解决性能问题,这在传统的性能测试中是最难以控制进度的。虽然通常来说,定位和解决性能问题是开发人员或者是架构师的职责,但是QA工程师也有责任帮助开发人员及架构师完成这一 巨的任务。为了解决这个问题,有必要在软件开发的早期,在单元测试阶段,就对被测单元进行性能测试,这称之为单元性能测试。

进行单元性能测试,可以发现以下性能问题。

1.方法的性能问题。软件的相当一部分性能问题是由于方法的性能不佳引起的。系统的性能瓶颈定位到方法,一般有两种表现,单次执行某个方法耗时过多,以及某个方法被调用的次数太多。

● 单次执行某个方法耗时过多。这个很容易理解,如果某个方法单次执行就需要花费几百毫秒甚至上千毫秒,那么使用该方法的功能点的性能一定好不到哪去;如果对某一功能点有很高的性能要求,那么就至少需要在单元测试通过后保证每个方法单次执行的时间都不能太长,尤其是那些被普遍使用的关键底层方法。

造成单次执行某个方法耗时过多的原因可能有:

➢ 方法中使用了效率很低的算法。

➢ 方法中执行了速度很慢的SQL语句。

➢ 方法中太多次地创建和销毁外部连接(例如数据库连接)。

➢ 方法中包含了大量的I/O。

● 某个方法被调用的次数太多。这是另一种情况,其实和第一种情况相比,最后产生的结果是一样的,就是执行某个方法的总耗时很多。这种情况往往是不良的算法导致的,需要对算法进行优化,有时可能需要在架构上做出调整。

● 对于方法的性能问题,还有一个需要关注的问题是,不同数量级、不同数据分布下不同算法的性能差异。对于算法的性能评价,一般包括时间复杂度和空间复杂度。前者基本决定了在不同数量级时算法耗时的多少,后者决定了不同数量级时算法耗费存储空间的大小。

● 关于算法的时间复杂度和空间复杂度,在各种算法分析的书籍中已有很详细的讨论,在此不做过多描述。了解了算法的复杂度之后,显然,不同的算法适合于不同的数量级和不同的数据分布(输入实例的初始状态),而通过单元性能测试,可以根据测试结果选择最适合于本系统的算法。

2.多线程并发问题。并发测试不应该等到系统测试阶段才做,那样会使得定位性能问题非常 难。在单元测试阶段就进行多线程并发测试,至少可以帮助发现两类问题,多线程并发执行时方法的性能,以及多线程并发执行时对共享资源的竞争。

● 多线程并发执行时方法的性能。就像传统的性能测试那样,可以通过并发测试得到多线程并发时方法的最大处理能力(例如A方法每秒可执行300次,B方法每秒可执行400次),以及在不同线程数时方法的平均执行时间、最大执行时间。虽然通常无法从系统预期的业务目标中分析得到方法应具备的最大处理能力、平均响应时间、最大响应时间的较为精确的值,但是这仍然有助于发现一些性能明显达不到要求的方法并优化它。

● 例如,一个即时通信系统要求每台登录服务器每秒能完成100次登录,而经过单元性能测试,多线程情况下验证用户名密码的方法最多能达到每秒执行90次,那么很明显,该方法需要被优化,即使该方法的最大处理能力刚好能达到每秒100次,这仍然不够,因为系统上的一次登录的开销要比执行一次该方法的开销大,因此至少要保证该方法每秒能执行100次以上。当然,有一点需要确保,就是在测试时用于调用被测方法的程序不能成为性能瓶颈,就像传统性能测试时要保证产生压力的客户端不能成为瓶颈一样。

● 多线程并发执行时对共享资源的竞争。多线程竞争共享资源引起的最常见的性能问题是锁和死锁。

➢ 所谓锁,是指当线程采用独占式方式来访问共享资源时,会在访问前对共享资源先加锁,以确保在该线程访问此共享资源时,其他线程无法访问此共享资源。举个例子,当多个线程需要并发修改数据库某张表中的同一条记录时,每个线程在修改前都会先给该表和该行记录分别加上表锁和行锁,然后再修改,而其他线程只有等锁释放后才能加上自己的锁,再进行修改。又如synchronized方法,线程在进入该方法前也要先取得锁,然后才能进入。由于锁的存在,多线程并发时各个线程必须排队执行,无法并行处理,使性能在一定程度上下降。

➢ 当然出现锁并不意味着性能一定存在问题,这个取决于锁释放的速度以及系统的并发量要求,如果锁释放得足够快,系统的并发量要求又不高,那么存在锁也没有问题,关键看测试的结果是否能满足性能指标。而且某些情况下,锁是必须的,否则会引起更多的问题,如果因此而导致性能达不到要求,可能需要变更需求,或者采用新的架构。

➢ 锁不一定是问题,但是死锁肯定是问题,是一定要避免的。死锁的产生,是由于多线程并发时,各个线程分别锁住了其他线程所需要的共享资源,都在等待对方释放共享资源,从而导致各个线程一直处于等待状态,不再执行,直到超时自动释放锁住的共享资源。死锁的危害无疑是巨大的,它可能导致系统在一定量的并发请求下不再处理任何的请求,对用户来说,这意味着系统 了。通过在单元测试时进行多线程并发测试,可以很容易地发现一部分死锁问题,从而在早期就予以解决,减少后期的工作量,降低项目的风险。

3.内存问题。内存问题不仅仅指内存泄漏,除了内存泄漏,还有另一类内存问题会对性能产生很大的影响,一般称之为短期对象循环。

● 内存泄漏:单元测试是解决内存泄漏问题的一个很好的阶段,这样能够避免在系统测试阶段乃至运行维护阶段进行冗长烦琐的搜集数据和分析工作。内存泄漏,就是某些内存区域被分配出去,在使用完后,由于开发人员的疏忽、架构设计的问题、所使用的第三方组件存在缺陷等因素,这些内存区域并没有被回收回来,无法继续再次被分配出去而继续使用。

简单地说,就是人认为这部分内存区域已经使用完了,机器应该回收,而机器认为这部分内存区域仍然在使用,因此并不回收,那么最后造成的结果就是,实际剩余可使用的内存要比人认为剩余的少得多。在Java中,一般称那些被使用完后由于疏忽而仍然被留在堆中的对象为游离对象。QA工程师在单元测试中的一个重要任务,就是在测试运行完成后,检查堆中是否存在游离对象,以此来检测被测的单元是否存在内存泄漏。

● 短期对象循环:这是在单元测试时能发现的另一大类内存问题。短期对象循环是指在一个方法执行期间创建了大量的对象,而当方法执行完成后,这些对象都将被垃圾回收,因而会导致频繁地垃圾回收,而每一次的垃圾回收都需要消耗大量的时间和资源,在并发量比较大时,甚至会引发内存不足。这个问题的危害同样很大,在应用负载高峰时,它会使得系统的性能急 下降,甚至崩溃。举个例子,如果你的Java应用连接数据库时不使用连接池,而是每次有数据库操作时,就新创建一个数据库连接,完成操作后立刻销毁该连接,当你的数据库操作比较频繁时,会导致大量的数据库连接很快地被创建、销毁,这会消耗掉相当大一部分的系统资源,极大地降低系统的性能,并且并发量越高,这部分消耗就越大,系统性能就越差。

在本书的第5章中,将会详细地介绍单元性能测试的相关内容,因此在这里不再做过多的描述。

1.4.2 测试驱动开发(TDD)

测试驱动开发(Test Driven Development,TDD)指测试推动开发活动的进行,要求开发人员在编写任何产品代码之前,首先定义针对产品代码的测试,编写代码时以使测试通过为目标。测试驱动开发过程中的测试是自动化的,在代码重构前后必须运行测试。测试驱动开发是一种在极限编程中处于核心地位的开发实践,是由极限编程的创始人Kent Beck提出的有别于以前先编码后测试的开发方法。

小资料

极限编程(Extreme Programming,XP)是1998年由Smalltalk社群中的大师级人物Kent Beck首先倡导的一种新型软件开发方法,它是一个周密而严谨的软件开发流程。它基于简单、交流、反馈、勇气的原则,在充分考虑到人的因素的前提下进行,达到客户的最大满意度。这种方法适用于中小型系统的开发。

XP属于轻量开发方法中较有影响的一种方法。轻量开发方法是相对于传统的重量开发方法而言。简单地理解,“量”的轻重是指用于软件过程管理和控制的、除程序量以外的“文档量”的多少。XP等轻量开发方法认识到,在当前很多情况下,按传统观念建立的大量文档,一方面需要消耗大量开发资源,同时却已失去帮助“预见、管理、决策和控制的依据”的作用。因此必须重新审视开发环节,去除臃肿累赘,轻装上阵。

XP项目提出了一共12个有效实践,包括完整团队、计划游戏、客户测试、简单设计、结对编程、测试驱动开发、改进设计、持续集成、集体代码所有权、编码标准、隐喻、可持续的速度。这些有效的实践使XP项目表现出比使用其他开发模式高得多的生产力。

XP这种轻量级的软件开发方法在软件质量上提出了极高的要求。XP非常强调代码的质量,并追求在规定的时间生产出满足客户需要的软件。测试驱动开发的实践正是极限编程的一个重要组成部分。

通过测试驱动开发,可以使开发人员清楚地看到自己工作结果的反馈,缩短设计决策的时间。在采用结对编程时,测试用例就是极好的对话 料,明确的测试用例可以帮助减少开发人员之间产生的分歧。在“红-绿-重构”这种简单有效的开发节 下,设计被精简,更加灵活。通过不断的重构,可以得到完全 合需求的设计方案,并且使得开发人员更加具有目标感。自动化的测试手段可以使工作张 有度。测试驱动开发能够增强开发人员的信心,敢于对系统进行大规模的重构。当然测试驱动开发最重要的功能还在于保障代码的正确性,能够迅速发现、定位缺陷。而迅速发现、定位缺陷是很多开发人员的 想。针对关键代码的测试集,以及不断完善的测试用例,为迅速发现、定位缺陷提供了条件。

测试驱动开发的基本过程包括:

1.明确当前要完成的功能。可以记录成一个TODO列表。

2.快速完成针对此功能的测试用例编写。

3.测试代码编译不通过。

4.编写对应的功能代码。

5.测试通过。

6.对代码进行重构,并保证测试通过。

7.循环完成所有功能的开发。

测试驱动开发一般遵循以下原则:

1.测试隔离。不同代码的测试应该相互隔离。对一块代码的测试只考虑此代码的测试,不要考虑其实现细节(比如它使用了其他类的边界条件)。

2.一顶帽子。开发人员开发过程中要做不同的工作,比如:编写测试代码、开发功能代码、对代码重构等。做不同的事,承担不同的角色。开发人员完成对应的工作时应该保持注意力集中在当前工作上,而不要过多的考虑其他方面的细节,保证头上只有一顶帽子。避免考虑无关细节过多,无谓地增加复杂度。

3.测试列表。需要测试的功能点很多,应该在任何阶段想添加功能需求问题时,把相关功能点加到测试列表中,然后继续手头工作。之后再完成对应的测试用例、功能代码,以及重构。这样既可以避免疏漏,也避免干扰当前进行的工作。

4.测试驱动。这个比较核心。完成某个功能,某个类时,首先编写测试代码,考虑其如何使用、如何测试,然后再对其进行设计、编码。

5.先写断言。测试代码编写时,应该首先编写对功能代码的判断用的断言语句,然后编写相应的辅助语句。

● 可测试性。功能代码设计、开发时应该具有较强的可测试性。其实遵循比较好的设计原则的代码都具备较好的测试性。比如较高的内聚性,尽量依赖于接口等。

● 及时重构。无论是功能代码还是测试代码,对于结构不合理、重复的代码等情况,在测试通过后,及时进行重构。

在测试驱动开发中,测试的范围应该如何定位?测试驱动开发强调测试并不应该是负担,而应该是帮助开发人员减轻工作量的方法。而对于何时停止编写测试用例,也是应该根据开发人员的经验,对于功能复杂、核心功能的代码,就应该编写更全面、细致的测试用例,否则只要测试流程即可。测试范围没有静态的标准,同时也应该可以随着时间而改变。对于开始时没有编写足够的测试用例的功能代码,随着缺陷的出现,根据缺陷把相关的测试用例补齐即可。

1.4.3 统一测试过程

统一测试过程的概念来源于Frank Cohen。在互联网应用软件开发与测试、软件测试自动化等方面,Frank Cohen具有非常丰富的经验和独到的见解,他提出一个概念,认为应当使软件开发人员、QA工程师和系统管理员作为一个单独的开发团队进行工作,三者紧密地结合、良好地协作,形成“troika”(一个俄语单词,描述了作为一组的三匹马并排拉车的情景),他甚至预计这将成为软件开发与测试的第4次进化。

在互联网应用软件开发过程中,软件开发人员、QA工程师和系统管理员都需要对应用程序进行测试,不过不同的角色关注的问题不同。

1.软件开发人员:在新的体系结构系统中,如何测试新的模块?

2.QA工程师:如何测试系统的可扩展性和功能?

3.系统管理员:如何测试系统的可靠性和可用性?

对于软件开发人员来说,单元测试可以很好地保证所编写的服务器端软件组件实现了正确的功能,但是某一些组件在单元测试之前需要有正确的状态。Frank Cohen认为,需要以下支持才能使这样的测试实现自动化:

1.一个可以编写测试的框架。

2.支持开发人员所使用的开发标准环境。

3.查看服务器性能的多协议支持,至少包括Web协议(HTTP和HTTPS)、Web Service协议(SOAP和XML-RPC)、电子邮件协议(SMTP、POP3和IMAP)。

4.每次更改模块时,有一个工具可以实现测试自动化。

以上4个目标都达成后,通过测试自动化框架和工具,软件开发人员可以快速编写自动化测试脚本,从而自动地将被测单元转化为正确的状态。这些自动化测试脚本实际上扩展了诸如JUnit这样的单元测试框架。

更重要的是,QA工程师可以使用这些自动化测试脚本对功能进行验证,在相同的测试环境中并发地运行自动化测试脚本以检验系统的可扩展性、并发性和可 复性。当QA工程师找到问题的时候,软件开发人员和QA工程师一同查看测试脚本日志,从而定位问题的位置并了解导致问题的原因。这样就将软件开发人员和QA工程师联合起来。

与此同时,系统管理员会经常采用上述的自动化测试脚本,并且长时间地运行它们。这些自动化测试脚本通过使用与实际用户相同的内部协议来驱动服务,脚本同时还记录了系统运行的情况,并且可以为系统管理员作出很好的服务质量报表。

至此,troika实现软件开发人员、QA工程师和系统管理员一起协同工作,所有三种角色都对应用程序进行了测试,并且自动化测试脚本被很好地复用。