持续交付:发布可靠软件的系统方法(十二)
- 第 12 章 数据管理
- 12.1 引言
- 12.2 数据库脚本化
- 12.3 增量式修改
- 12.3.1 对数据库进行版本控制
- 12.3.2 联合环境中的变更管理
- 12.4 数据库回滚和无停机发布
- 12.4.1 保留数据的回滚
- 12.4.2 将应用程序部署与数据库迁移解耦
- 12.5 测试数据的管理
- 12.5.1 为单元测试进行数据库模拟
- 12.5.2 管理测试与数据之间的耦合
- 12.5.3 测试独立性
- 12.5.4 建立和销毁
- 12.5.5 连贯的测试场景
- 12.6 数据管理和部署流水线
- 12.6.1 提交阶段的测试数据
- 12.6.2 验收测试中的数据
- 12.6.3 容量测试的数据
- 12.6.4 其他测试阶段的数据
- 12.7 小结
第 12 章 数据管理
12.1 引言
对于测试和部署过程来说,数据及其管理与组织会带来一些特定的问题,原因有两个。首先,一般来说,测试中会涉及庞大的数据量。分配给应用程序本身(它的源代码和配置信息)的空间通常远远比不上记录其状态的数据量。其次,应用程序数据的生命周期与系统其他组成部分的生命周期是不同的。应用程序数据需要保存起来。事实上,数据通常要比创建和访问这些数据的应用程序的寿命长。而重点则在于,当系统升级或回滚时,需要保存并迁移数据。
大多数情况下,当部署新代码时,可以删除前一个版本,并用新版本完全代替旧版本。这样可以确定部署的初始状态。尽管在某些情况(很少)下,对数据这么做也是可行的,但在现实中,大多数系统无法使用这种方式。一旦将某个系统发布到了生产环境中,与其相关联的数据就会不断增加,并以其自己特定的形式提供着巨大的价值。甚至可以说,它是系统中最有价值的一部分。当我们需要修改结构或内容时,问题就来了。
随着系统的发展和演进,这类修改是不可避免的。因此,我们必须找到某种机制,既允许变更,同时又能使损失最小化,让应用和部署流程的可靠性更高。这其中的关键就是将数据库迁移过程自动化。现在有一些工具对数据迁移的自动化提供了较多的支持,可以将这部分工作当做自动化部署过程的一部分进行脚本化。这些工具还允许你对数据库进行版本化管理,从一个版本迁移到另一个版本。这对开发过程与部署过程之间的解耦有促进作用。尽管你不会每次修改数据库模式之后都进行部署,但每次需要修改数据库时就应该创建一个迁移脚本。它也意味着,数据库管理员(DBA)不需要做很多的预先计划,而是与应用程序的演进一样,也可以进行增量式的工作。
在本章内容开始之前需要说明的是:绝大多数应用程序依赖关系型数据库管理它们的数据,但这并不是存储数据的唯一方法。对于所有应用场景来说,这也不一定是最佳选择,NoSQL运动的崛起说明了这一点。本章所提供的建议与数据存储系统本身无关,但当讨论到细节时,我们会谈到关系型数据库系统,因为对于应用程序来说,它毕竟还是数据存储系统的绝对主力军。
12.2 数据库脚本化
与系统中其他变更一样,作为构建、部署、测试和发布过程的一部分,任何对数据库的修改都应该通过自动化过程来管理。也就是说,数据库的初始化和所有的迁移都需要脚本化,并提交到版本控制库中。无论是为开发人员创建一个新的本地数据库,还是为测试人员升级系统集成测试(Systems Integration Testing,SIT)环境,或者作为发布过程的一部分迁移生产环境中的数据库,都应该能够使用这些脚本来管理交付流程中的每个数据库。
当然,数据库的模式会随着应用程序不断演变。这就引出了一个要求,即某个版本的数据库模式应该与该应用程序的某个具体版本相对应。例如,当做试运行环境的部署时,就要能够把试运行环境的数据迁移到适当的数据库模式上,以便与正在部署的新版本应用程序相匹配。通过对脚本的细心管理可以让这项工作成为可能,参见12.3节。
最后,数据库脚本也应该作为持续集成过程的一部分来使用。尽管根据定义,单元测试的运行不需要数据库,但对使用数据库的那些应用程序进行的验收测试都要求数据库能够被正确地初始化。因此,在验收测试中,环境准备(setup)过程中应该包括创建与应用程序的最新版本相匹配的正确的数据库模式,并加载必要的测试数据,以便运行验收测试。在部署流水线的后续阶段中也可以使用类似的过程。
初始化数据库
在这种交付方式中,一个极其重要的方面就是:能够以自动化方式重新建立一个应用程序的运行环境。如果做不到这一点,就无法断定系统的确是以期望的方式运行的。
在整个开发过程中,应用程序不断变化,而数据库部署这方面是最容易做对,也是最容易维护的。几乎所有的数据管理系统都支持通过自动化脚本进行数据存储的初始化工作,包括数据模式和用户认证。所以,创建和维护一个数据库初始化脚本只是起点。脚本应该首先创建数据库结构、数据库实例和模式,等等,然后再向数据库上添加数据表及应用程序启动时所需的数据。
当然,和代码一样,这个脚本以及与维护数据库相关的其他脚本都要保存到版本控制库中。
对于一些简单的项目,这些就足够了。比如,对于那些操作数据集只是某种暂存方式(transient)的项目,或者数据是预定义好的那些项目(比如,某个系统在运行时把数据库仅作为只读的数据源),只要清除前一个版本,并用一份新的副本代替它,或者从已存储的版本中重新创建一份新的数据就行了。这是一种简单有效的策略。如果有条件这么做的话,就没什么可犹豫的了。
简而言之,部署一份新数据库的过程如下。
- 清除原有的数据库。
- 创建数据库结构、数据库实例以及模式等。
- 向数据库加载数据。
但在大多数项目中,数据库的使用比这复杂得多。我们要考虑一下更复杂也更常见的情况,即当使用一段时间后,需要对数据库进行修改。在这种情况下,现存的数据的迁移必须作为部署过程的一部分。
12.3 增量式修改
持续集成要求在每次修改应用程序后,它都能够正常运行。这也包括对数据结构和数据内容的修改。持续交付要求我们必须能够部署应用程序的任意一个已通过验证的版本(包括对数据库变更的版本)到生产环境(对于用户自行安装且包含数据库的软件也是一样的)。除了那种最简单的系统,对数据库进行更新的同时,还要保留它们的数据。最后,由于在部署时需要保留数据库中的已有数据,所以需要有回滚策略,以便当部署失败时使用。
12.3.1 对数据库进行版本控制
以自动化方式迁移数据最有效的机制是对数据库进行版本控制。首先,要在数据库中创建一个,用来保存含它的版本号。然后每次对数据库进行修改时,你需要创建两个脚本:一个是将数据库从版本x
升级到版本x+1
(升级脚本),一个是将数据库版本x+1降级到版本x(回滚脚本)。还需要有一个配置项来设置应用程序使用数据库的哪个具体版本(它也可以作为一个常量放在版本控制库中,每次有数据修改时更新一下)。
在部署时,可以用某种工具来查看当前部署的数据库版本以及将要部署的应用程序所需要的版本。然后再找到需要运行哪个脚本将数据库从当前版本迁移到目标版本,并依据顺序在数据库上执行它。对于升级来说,它会按正确的顺序执行所有的升级脚本,从最老的到最新的;对于降级来说,它会以相反的顺序执行对应的降级脚本。Ruby On Rails本身就以ActiveRecord迁移这种方式提供了这种技术。如果用Java或.NET,我们的同事开发了一个简单的开源应用叫做DbDeploy(对应的.NET版本是DbDeploy.NET)可以为你管理这一过程。还有其他几种解决方案也能做类似的事情,包括Tarantino、微软的DbDiff和IBatis的Dbmigrate。
下面是一个简单的例子。当开始写应用程序时,就写下第一个SQL文件,文件名为
1_create_initial_tables.sql:
CREATE TABLE customer ( id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, firstname VARCHAR(255) lastname VARCHAR(255)
);
在代码的后续版本中,发现需要向表中增加客户的生日,因此,创建了另一个脚本,名为2_add_customer_date_of_birth.sql,其中描述了如何增加这一列,以及如何回滚:
ALTER TABLE customer ADD COLUMN dateofbirth DATETIME;
--//@UNDO
ALTER TABLE customer DROP COLUMN dateofbirth;
在//@UNDO这个提示之前的代码代表如何把数据库从版本1升级到版本2。该提示之后的代码表示如何把数据库从版本2回滚到版本1。这是DbDeploy和DbDeploy.NET所用的语法。
如果升级脚本是向数据库中增加新结构的话,写回滚脚本并不难。回滚脚本只要先删除引用约束,再删除它们就行了。通常,也有可能为修改已有结构的那些变更创建一个相应的回滚脚本。然而,在某些情况下,必须删除数据。但我们仍旧有办法写出无损的升级脚本。在从主表中删除它们之前,让脚本创建一个临时表,把数据复制到其中。当你这么做时,还必须复制该表的主键,以便数据能够复制回来,并由回滚脚本重建约束。
有时会有一些特别的限制,使你无法很容易地对数据库进行升降级。根据我们的经验,导致困难的最常见问题是修改数据库模式。如果这种修改是增加东西,并创建新的关联,那么问题不大,除非是已存在的数据违反了将要增加的某个约束,或者增加了新对象,但没有默认值。如果模式的修改是减东西,问题就出来了,因为一旦弄丢了某条记录与其他记录之前的关联关系,就很难重建这种关系了。
管理数据库变更的技术需要达到以下两个目标:首先,要能够持续部署应用程序而不用担心当前部署环境中所用的数据库版本。其次,部署脚本只要将数据库向前或向后更新到与应用程序相匹配的版本即可。
另外,这在某种程度上也使得数据库的修改和应用程序之间解耦了。DBA可以写数据库迁移脚本,并把它提交到版本库中,而不必担心会破坏应用程序。为了做到这一点,DBA只要保证把这些脚本作为向新版本数据库迁移工作的一部分就可以了。这样,直到有产品代码需要用到这个数据库新版本时,才会用到这些数据库升级脚本。而此时,开发人员只要确定需要将数据库升级到哪个版本就行了。
12.3.2 联合环境中的变更管理
在很多组织中,所有应用程序常常通过一个数据库互相集成。我们并不推荐这么做,最好是让这些应用程序直接交互,并找出在什么地方需要公共的服务(就像面向服务架构里的做法那样)。然而,有些情况下直接通过数据库集成也是合理的,或者因为架构改造工作太多,所以无法修改应用程序的架构。
在这种情况下,对数据库的一次修改可能会对使用该数据库的其他应用程序引起连锁反应。首先,在一个联合环境(orchestrated environment)中对这种变更进行测试是非常重要的。换句话说,这个环境中的数据库应该近似于生产环境,并且使用该数据库的其他应用程序也应该在该环境中运行。这种环境通常被叫做系统集成测试环境,或试运行环境。利用这种方法,如果这些测试在这种有其他应用程序的环境中频繁运行,你很快就能发现,是否对其他应用程序有影响。
在这种环境下,对“哪个应用使用了哪个数据库对象”进行登记是个很有效的方法,这样你就知道哪次修改会影响哪些应用程序了。
我们曾经看到过这样一种做法。通过对代码库进行静态分析自动生成各应用程序相关的数据库对象列表。这个列表的生成是每个应用程序构建过程的一部分,其结果对其他应用程序是公开的,这样,就很容易知道修改是否会影响其他的应用程序。
最后,要确保在修改时已与维护其他应用程序的团队达成一致。管理增量修改的一种方法是让应用程序与数据库的多个版本兼容,以便数据库的迁移与依赖于它的那些应用程序相对独立。对于那种无停机发布(Zero-Downtime Release)来说,这种技术也很有用。
12.4 数据库回滚和无停机发布
一旦应用程序的每个版本有了前面几节所说的升级和回滚脚本,使用DbDeploy这类工具在部署时将已有数据库迁移到与应用程序版本相对应的状态就比较容易了。
然而,有一个特例,那就是生产环境的部署。生产环境的部署有两个常见的需求会成为额外的约束。一是当回滚时需要保留本次升级后产生的数据,二是根据签订的SLA,要保持应用程序的可用状态,也叫做热部署或无停机发布。
12.4.1 保留数据的回滚
在回滚时,回滚脚本(如前所述)通常被设计成可以保留执行升级后产生的数据。如果回滚脚本满足下面的条件,就应该没有什么问题。
- 它应该包括模式修改,即不丢失任何数据(比如范式化或非范式化,或者在表间移动列)。在这种情况下,只要运行回滚脚本就行了。
- 它只删除新版本使用的那些数据,假如这些数据丢失了也没什么大问题。在这种情况下,只要运行回滚脚本就行了。
然而,有些时候简单地运行这些回滚脚本是不行的,如下所述。
- 回滚涉及从临时表中将数据导回来。此时,由升级而新增的数据记录会破坏集成约束。
- 回滚要删除那些旧版本系统无法接受的数据。
在这种情况下,有几种方法可以将应用程序回滚。
一种方法是将那些不想丢失的数据库事务(transaction)缓存一下,并提供某种方法重新执行它们。当升级数据库和应用程序到新版本时,确保记录了每次在新版本上发生的事务。可以通过记录来自UI的所有事件,比如通过拦截系统各组件间传递的较粗粒度的消息(如果应用程序使用了事件驱动范式,就会相对容易一些),或真实地复制事务日志中发生的每个数据库事务。一旦应用程序被成功地重新部署,这些事件就可以被重新播放一遍。当然,这种方法需要细心地设计和测试以确保它可以发挥作用。如果真的需要确保回滚时不丢失数据,这也是一种可接受的做法(tradeoff)。
如果使用蓝—绿部署(参见第10章)的话,可以考虑第二种方法。提示一下,在蓝—绿部署环境中,应用程序会同时有新旧两个版本同时运行,一个运行于蓝环境,另一个在绿环境。“发布”只是将用户请求从旧版本转到新版本上,而“回滚”只是再把用户请求转到旧版本上而已。
如果使用蓝—绿部署方法,在发布时就要为生产数据库(假设它是蓝数据库)做一个备份。如果数据库不允许热备份,或者有其他原因无法这么做的话,就要将应用程序切换到只读状态,以便能够执行备份。然后,这个备份被放在绿环境中,并在其上执行迁移操作。然后,再把用户切换到绿环境上。
如果要执行一次回滚,那么只要将用户切换回蓝环境即可。之后,可以把在绿环境的数据库上发生的新事务收回,要么在下一次更新之前重新应用这些新事务到蓝数据库上,要么再次升级之后马上应用这些事务。
有些系统的数据太多,如果没有较长的停机时间,几乎无法执行这类备份和恢复操作。此时就不能用这种方法了。尽管使用蓝—绿环境仍旧可行,但需要在发布时切换到共同的数据库上,而不是使用自己的独立数据库。
12.4.2 将应用程序部署与数据库迁移解耦
还有第三种方法可用于管理热部署,那就是将应用程序部署过程与数据库迁移过程解耦,分别执行它们,如图12-1所示。
这种方法也适用于联合环境的变更管理,以及第10章提到的蓝—绿部署和金丝雀发布模式上。
如果能频繁发布,那么就不需要每次发布应用程序时都迁移数据库。当真的需要迁移数据库时,不能只让应用程序仅仅和新版本的数据库相匹配,还要确保应用程序既适应于该新版本,也适合于当前运行的版本。在图12-1中,版本241既能与当前部署的数据库版本14相匹配,也能在数据库的新版本15上运行。
先部署应用程序的一个过渡版本,让它可以与当前版本的数据库一起工作。然后,当确信新版本的应用程序非常稳定且不必回滚时,就可以将数据库升级到新的版本(如图12-1中的版本15)。当然,在这么做之前,要对其进行备份。之后,当应用程序的下一个版本准备好部署时(图12-1中的版本248),就可以直接部署,而不必迁移数据库了。这个版本的应用程序只要与数据库的版本15相匹配就行。
当很难将数据库回滚到某个较早的版本时,这种方法也很管用。有一次,我们在对数据库的新版本做很大修改时(包括修改了数据库模式,丢失了一些数据),就曾用过这种方法。当发生问题后,通过再次升级操作回滚到早期版本的效果。我们部署了应用程序的一个新版本(它是向后兼容的,可以运行在早期版本的数据库上),但没有部署新的数据库变更。我们观察了一下这个新版本,确认它没有引入问题。最后,当我们有把握后,就部署了数据库的变更。
虽然对于那些常见的修改来说,向前兼容性是一个可采纳的有效策略,但它不是一种通用的解决方案。在这里,“向前兼容性”是指应用程序的早期版本仍旧可以工作在后续版本的数据库上的一种能力。当然,如果在新的模式中有新增的字段或表,那么它们会被该版本的应用程序忽略。只不过,两个数据库版本的共同部分还是一样的。
对于大多数变更来说,最好将下面这种方法作为默认方法,即大多数修改应该是增加操作(比如向数据库中增加新表或字段),尽可能不修改已存在的结构。
另一种管理数据库变更和重构的方法是以存储过程和视图的形式来使用抽象层。如果应用程序是通过这种抽象层来访问数据库的,就可以修改底层的数据库对象,同时用视图和存储过程的一致性为应用程序提供接口。
12.5 测试数据的管理
测试数据对于所有测试(无论是自动化测试还是手工测试)来说,都非常重要。什么样的数据能让我们模拟与系统的互操作呢?用什么数据表示边界用例,来证明应用程序在非正常输入时仍旧可以工作呢?什么样的数据会让应用程序进入到错误状态,以便我们可以评估在这种条件下它的响应呢?这些问题与我们对系统进行各层次的测试都相关,也对数据库中的测试数据有依赖的那些测试造成了一些特定的问题。
在本节中将重点讨论两点。
- 首先是测试性能。我们想确保测试尽可能快地完成。就单元测试而言,要么根本不要依赖数据库来运行,要么运行在一个内存数据库上。对于其他类型的测试,就要细心管理测试数据了,一定不要使用生产数据库的一个dump,除非有特殊情况。
- 其次就是测试的独立性。理想的测试应运行在已定义好的环境中,其输入应该是受控的,这样我们才能很容易地评估它的输出。另一方面,数据库是信息的持久存储,每次测试可能会修改其持久化内容,除非采取某些措施,阻止这样的事情发生。否则的话,这会导致起始条件不清晰,尤其是当无法直接控制测试的执行顺序时。可遗憾的是,事实往往就是这样的。
12.5.1 为单元测试进行数据库模拟
单元测试不使用真正的数据库是非常重要的。通常单元测试会使用测试替身对象来取代与数据库打交道的服务。如果做不到这一点(比如你想测试这些服务)的话,你可以用另外两种策略。
- 一是用测试替身对象来替代那些访问数据库的代码。
- 使用假的数据库。
12.5.2 管理测试与数据之间的耦合
当涉及测试数据的时候,测试套件中的每个测试都有其依赖的某个状态,这一点非常重要。当用“given, when, then”
的格式写验收条件时,“when”就是测试开始时的所处的状态。只有当开始状态为已知状态时,你才能将它与测试运行结束后的状态相对比,来验证该测试用例所测试的行为。
就单独一个测试而言,做到这一点非常简单。然而,对于整个测试套件来说,就要思考一下如何才能做到这一点了,尤其是对数据库有依赖的那些测试。
总的来讲,有以下三种方法可以用来做测试设计,以便管理好数据的状态。
- 测试的独立性(test isolation):合理地组织测试,以便每个测试的数据只对该测试可见。
- 适应性测试(adaptive tests):按如下方式进行测试设计——每次运行时先对数据环境进行检查,然后使用这些检查中得到的数据作为数据基础,对系统行为进行测试。
- 测试的顺序性(test sequencing):按如下方式进行测试设计——按某种已知的序列运行,每个测试的输入依赖于前一个的输出。
通常,我们强烈推荐使用第一种方法。测试之间彼此独立不但带来了更高的灵活性,而且更重要的是,能够通过测试的并行执行来优化测试套件的性能。
虽然另外两种方法也是可行的,但根据我们的经验,它们的扩展性不佳。随着测试套件不断变大,其中的交互越来越复杂,这两种方法都有可能导致一些很难发现和修复的失败。测试之间的交互变得让人越来越费解,而维护这种测试套件的成本也会不断增加。
12.5.3 测试独立性
测试独立性是指确保每个测试都具有原子性。也就是说,每个测试不应该用其他测试的结果建立它的初始状态,并且其他测试也不应该以任何形式影响该测试的成功或失败。对于提交测试(甚至那些将测试数据持久化到数据库中的测试)来说,达到这种独立性是相对容易的。
12.5.4 建立和销毁
无论选择的策略是什么,在测试运行之前建立一个已知的状态良好的起始点,并且在其运行结束时再重建这个起始点是至关重要的,可以避免测试间依赖(cross-test dependency)。
对于具有良好独立性的测试,在测试准备阶段通常会用相关的测试来填充数据库。这可能包括创建一个新的数据库事务(以便在测试结束后执行回滚),或者只是插入几条特定测试的信息。
为了在测试开始时建立一个已知状态良好的起点,适应性测试(adaptive test)会检查并评估一下数据环境。
12.5.5 连贯的测试场景
常常有这样一种倾向,即创建一个连贯的“故事”(将多个测试场景串在一起),让一些测试顺序执行。这种方法的出发点是已创建的数据是有连续性的,这样可以将测试用例的建立和销毁工作最小化。而且,每个测试本身也会简单一点儿,因为它不再负责管理自己的测试数据了。另外,作为一个整体,测试套件运行得更快,因为它不用花太多时间创建和销毁测试数据了。
有时候,这种做法很诱人,但在我们看来,这是应该予以抵制的一种诱惑。这种策略的问题在于我们正在努力把一个连贯的故事与测试紧紧耦合在一起。这种紧耦合有几个非常大的缺点。随着测试套件的增长,测试的设计越来越难。当一个测试失败以后,会对后续依赖于它的一系列测试造成影响,让它们也失败。业务场景或技术实现的变更可能导致重写测试套件,非常痛苦。
12.6 数据管理和部署流水线
对于自动化测试来说,创建和管理数据的开销可能非常大。让我们退一步,看看测试应该关注哪些点?
我们通过测试来断言我们所开发的应用程序的行为符合我们期望的结果。我们运行单元测试来避免刚做的修改破坏已有的应用程序。我们运行验收测试来断言应用程序交付了用户所期望的价值。我们执行容量测试来断言应用程序满足我们的容量需求。可能,我们还会通过运行一套集成测试来确认应用程序与其依赖的第三方服务可以正常通信。
那么,对于部署流水线的每个测试阶段,我们需要哪些测试数据,并应该如何管理它呢?
12.6.1 提交阶段的测试数据
提交测试是部署流水线的第一步。提交测试的快速运行对这个流程来说是非常关键的。提交阶段的运行时间就是开发人员在进行下一步工作之前需要等待的时间。这一阶段每增加30秒都会产生很多成本。
除了要立即执行以外,提交测试还是防止因疏忽大意而修改了系统的主要防御手段。这些测试对实现细节的依赖越严重,它们达到这一目的就越难。原因是,当需要重构系统某些方面的实现细节时,你希望这些测试能提供一些保护。如果这些测试与具体实现牢牢地绑定在一起,那么具体实现上的一点点改动都不得不修改与它相关的很多测试。那些与具体实现紧耦合的测试将阻碍修改,而不是防护系统的行为,从而便于必要的修改。如果在具体实现上仅做了一个小的改动就被迫对测试进行大量的修改,那么,作为一种系统行为的可执行规范,这些测试就没有有效地完成它应该做的工作。
这也是持续集成过程发布一些看似不相关的积极行为的一个关键点。好的提交测试会避免复杂的数据准备。如果你发现自己很难为某个测试准备数据的话,这是一个明显的信号,表示你的设计需要更好地解耦。要将设计分成多个互相独立的组件和测试,使用测试替身对象来模拟依赖
最有效的测试不是真正的数据驱动的,它们应用使用最少的测试数据来断言被测试的单元正确完成了所期望的功能。创建那些的确需要复杂数据来展现期望行为的测试时应该尽可能小心一些,通过重用测试辅助类或夹具来创建它,以防止系统中数据结构设计的变化对系统的可测性带来灾难性的影响。
12.6.2 验收测试中的数据
与提交测试不同,验收测试是系统测试。这意味着,它们的测试数据必然会更复杂,如果你想避免测试变得非常笨重,需要更细心地管理这些测试数据。也就是说,其目标是尽可能减少测试对大型复杂数据结构的依赖。方法基本上与提交阶段的测试一样:我们希望在测试用例的创建方面做到一些重用,并将每个测试对测试数据的依赖最小化。我们应该创建恰好够用的数据,用来验证我们对系统的期望行为。
当考虑如何为某个验收测试准备应用程序的某个状态时,区分以下三类数据是非常有用的。
- 测试的专属数据(test-specific data):那些在测试中用于驱动应用程序行为的数据。它代表了测试中这个用例的细节。
- 测试的引用数据(test reference data):这类数据通常是附加的,与某个测试相关,但是并不真正与被测试的行为相关。测试中需要它,但只是对该测试的一个支持,而不是主角。
- 应用程序的引用数据(application reference data):经常有一类数据,它们与被测试的行为无关,但是是应用程序运行所必需的。
- 测试专属数据应该是唯一的,而且要使用测试独立策略,以确保测试能够在已知状态(不受其他测试结果影响)的环境上开始执行。
- 测试引用数据可以通过使用预填充种子数据的方式来管理。这些种子数据在不同的测试中被重用,用来建立某种测试所使用的通用环境,但通用环境应保持不受测试操作的影响。
- 应用程序引用数据可以是任何值,甚至可以是Null,它对测试的输出没有任何影响。应用程序引用数据可以使用数据库转储这种形式来提供,如果适当的话,测试引用数据(应用程序启动所必需的)也可以这么做。当然,要对这些数据进行版本控制,并确保它们作为应用程序准备过程的一部分进行数据迁移。这对测试自动化数据库迁移策略也是很有效的方式。
12.6.3 容量测试的数据
容量测试用来指出应用程序所需的数据规模问题。该问题在两方面体现:(1) 为测试提供足够的输入数据;(2) 准备适当的引用数据来支撑测试中的多个用例。
我们把容量测试看做验收测试的重复利用,只是同时运行很多用例而已。如果应用程序有“发订单”(placing an order)这一操作的话,那么在做容量测试时,我们就希望能同时模拟发很多订单。
我们倾向于使用像交互模板(详见9.6.3节)这样的机制自动生成大量数据,包括输入数据和引用数据。
事实上,为了做验收测试,我们需要创建并管理一批数据。通过上述方法,我们能够对这些数据进行扩容。而且也应该尽量使用这种数据重用策略。我们的理论依据是,验收测试套件中的这类交互以及与这些交互相关的数据就是系统行为的可执行规范。如果验收测试的确履行了这种职责,那么它们就覆盖了应用程序所支持的重要交互行为。假如作为容量测试的一部分,它无法代表我们想要度量的系统主要行为的话,那这里就有问题了。
另外,如果我们已经有了某种机制和流程,使得在应用程序演进的过程中,这些测试能够与应用程序保持同步,那么在做容量测试,或者某种验收测试阶段之后的任何阶段测试时,为什么还要重新转储所有数据,从头再来一次呢?所以,我们的策略是把验收测试当做与系统交互的记录,把这些记录作为后续测试阶段的起点。
对于容量测试,我们利用一些工具,将所选定的验收测试与相关数据关联在一起,再将它扩展成很多不同的“用例”,这样,我们就可以利用一个测试就模拟出与系统的很多交互了。
这种生成测试数据的方法,让我们可以将原本要花在容量测试数据管理方面的精力集中在各个交互中心而且唯一的核心数据上。
12.6.4 其他测试阶段的数据
抛开具体的实现技术,至少从设计理念上来讲,在验收测试阶段之后的所有自动化测试阶段中,我们都可以使用同样的方法。我们的目标是重用那些自动化验收测试所用的“行为规范”作为其他测试(不仅限于功能性测试)的起点。
当创建Web应用时,我们利用验收测试套件不但可以衍生出容量测试,而且可以衍生出兼容性测试。对于兼容性测试来说,我们在所有流行的Web浏览器上重新运行了所有的验收测试套件。这并不是一个详尽的测试,比如它无法测试可用性,但是如果某个修改在某个浏览器上破坏了用户交互的话,它就会给我们一个报警。由于我们重用了部署机制和验收测试套件,并且使用虚拟机来运行测试,所以,除了运行测试时使用的CPU时间和磁盘空间之外,兼容性测试几乎是免费的。
12.7 小结
由于生命周期不同,数据管理也面临一些待解决的问题。尽管这些问题与部署流水线上下文中的问题有所不同,但管理数据所用的基本原则是一样的。关键是要把创建和迁移数据库全部变成自动化过程。这个过程是部署流程的一个组成部分,确保它的可重复性和可靠性。无论是将应用程序部署到开发环境或包含最小数据集的验收测试环境,还是作为部署的一部分将生产数据集迁移到生产环境中都要使用相同的过程。
即使有自动化的数据库迁移过程,细心地管理测试数据仍旧是非常必要的。尽管直接使用生产数据库的副本是一个充满诱惑力的选择,但通常会因为数据太大而不易使用。相反,应该让测试自己创建它们所需的状态,并确保每个测试都独立于其他测试。甚至做手工测试时,也很少使用生产环境中数据库副本,它不是最佳起点。测试人员应该根据测试目的创建并管理自己的最小数据集。
本章中提到了如下一些重要原则与实践。
- 对数据库进行版本管理,使用DbDeploy这样的工具管理数据迁移过程的自动化。
- 努力保持数据库模式修改的向前和向后兼容性,以便把数据的部署和迁移问题与应用程序的部署问题分开。
- 确保在准备过程中,测试可以创建它们所依赖的数据,并确保数据是分开的,以保证不会影响那些同时运行的其他测试
- 只保存不同测试之前应用程序启动所需要的测试数据,以及一些非常通用的引用数据。
- 尽可能使用应用程序的公共API为测试创建正确的初始状态。
- 在大多数据情况下,不要在测试中使用生产数据集的副本。创建自定义数据集既可以通过细心选择生产数据集的最小子集来实现,也可以通过运行验收测试和容量测试来实现。