测试模式点滴:准备环境

准备实践自动化测试的朋友可以看看这本书:XUnit Test Patterns——Refactoring Test Code,及其网站http://xunitpatterns.com/。估计国内还没有译本。书的内容不错,只是倾向于单元测试,还糅合了其它诸如“反测试模式”(文中称为bad smells,大概如同code complete里面描述的bad smells差不多,只是这里从自动化测试代码的角度而言)的内容。我对测试模式比较感兴趣,就采摘一些与大家共享。

任何自动化测试用例都免不了先执行一段代码用来做准备(原文是fixture,天晓得怎么直译,就叫准备吧):初始化环境,准备数据,初始化需要测试的对象状态等等。当然也有最简单的那种什么都不用准备的——最简单的就犯不着讨论了,是吧——上来就用不同参数调用要测试的函数。如果是为了实现高覆盖率和良好的可重用性、可维护性的话,准备环境就不是一件轻松的活。

有多不轻松?比如说我准备了上百组参数,这样覆盖率足够了。每组调用要测试的函数一次,于是我就用个循环,执行一次,全对了。过两天发现还漏了几组参数,加上以后发现,喔,原先对的现在不对了。原来有些调用改变了重要的状态,后加的参数暴露了这个问题。那怎么办呢,看来每次调用之前都要重新设置状态了。

于是简单的循环不够,得加入一段重置代码。这样问题消失了。

过几天要测试的函数多了几个,内部状态变复杂了。于是我发现用一些参数调用函数时需要这种状态为前提,用另一些参数调用函数时又需要那种状态为前提。所以,重置状态的代码就不能用了,每个函数调用都需要指定设置为什么状态了。

你或许觉得你不会遇到这么复杂的问题。那看看这样一个项目,后台数据库的数据以各种形式的图表表达在前台界面上,要测试的函数的功能是把数据画在界面上,不同的参数反映了不同的画法。过几天为了提高性能,经常用到的图表会缓存在某个地方。于是内部状态就跟测试用例及其顺序有关系了。

困难还可以演变的更大。更多的测试用例是为了检验数据库里面不同的数据组合在图表上有无问题,自从有了它们,原来的测试用例就不得不修改了,因为它们依赖于数据库里面不一样的数据。这样我还得清除掉数据库里面不用的数据并写入我需要的数据。因为数据库操作是如此的慢,所以原先两分钟就能跑完的上百组测试,现在需要两个钟头。因为要花那么多的时间来执行,开发人员没办法在提交代码之前跑完,于是要么他们不再执行,爆出一大堆bug,要么为了等测试跑完浪费了很多时间,没办法按时发布产品,大伙儿一块完蛋。

解决方案就是各种准备环境的测试模式。与设计模式一样,不同的模式解决不同的问题。

l 最小化准备

n 为所有测试用例都需要的准备工作实现一个函数,这个函数可以利用各种编程语言的能力来实现“一处指定,到处调用”,也就是一个地方标明需要调用这个函数,所有测试用例都能预先调用这个函数。

l 共享准备

n 总有若干测试用例需要共同的准备工作,它们可以通过实现一个函数共享这些逻辑。

n 风险在于难以抵抗这样的诱惑:为了支持更多的测试用例从而让共享准备越变越大越复杂。

l 即时准备

n 每个测试用例都把要准备的东西重新创建出来。

n 绝大多数解决方案都倾向于这种方法,这是因为,程序员总是爱写代码的,呵呵。

n 风险在于,如果准备的代码比测试用例代码还大还复杂怎么办?

l 永久准备

n 状态用文件的方式表达,在准备代码的指引下合适的装入内存、磁盘或者数据库。

n 如果文件表达的方式更简单,人们就倾向于使用数据驱动(data driven)的方式;如果文件已经存在,人们就倾向于把它们放到合适的地方而不是用代码生成;如果数据操作费时,人们就倾向于把大部分的数据用数据库导入的方式来准备——这就是为什么有人愿意用这种做法。

n 风险在于,如果准备文件的工作量也很大,人们就会陷入两难:代码执行慢,准备文件也慢。

l 生成器准备

n 很复杂的数据需要使用代码来生成,这样可以很快的产生大量测试数据。

n 比如说为了测试解码器,需要一个产生各种编码的编码器

n 尽量多用现成的,少开发新的吧:你怎么确保这个生成器是没有bug的呢?

l 延迟准备

n 一个测试用例可能在中途步骤就因为产品的bug验证失败,如果有些准备工作是后边步骤才需要的,那就没有必要一开始就做。

n 这是解决测试执行性能问题的一个小技巧。

l 洋葱头准备

n 原文叫做decorator,跟设计模式里面的decorator一个意思。准备工作也是存在继承关系的,某一组测试用例A要用到准备工作aB则用到b,如果b包含了a,那么b直接调用a好了。

n 举个例子,测试用例A用到数据库但不关心里面的数据,B则要求里面至少有某一条记录,那么B的准备工作就是一个洋葱,最里面是创建数据库,外层则是插入需要的记录,甚至C需要在具备这个记录的同时数据库还有一个备份,那就再包一层好了。

l 分组准备

n 依赖同一种准备工作的测试用例都放在一个分组里面。

n 这就是最终的解决方案,同类的放在一起处理就不会因为切换而花时间,调试的时候也容易分离症状。

所以就前面提到的例子来说,解决方案就是:

l 准备一个最基本的数据库,导出成为文件(最小准备,永久准备)

l 根据准备工作的不同对测试用例分类(分组准备)

l 提取可以共享的准备工作实现相应的代码,如果发现有继承关系可以用继承的方式组织这些类(共享准备,洋葱头准备)

l 确保导入数据库文件的代码是洋葱头的最里面,除非某些测试用例需要延迟导入(延迟准备)

有准备就有清理,下次我们谈谈清理环境(原文是fixture teardown)。

你可能感兴趣的:(设计模式,编程,工作,单元测试)