测试驱动数据库开发

978-7-115-34628-5
作者: 【美】Max Guernsey III
译者: 伍斌
编辑: 杨海玲

图书目录:

详情

测试驱动开发(TDD)的实践已经帮助众多软件开发人员提高了软件开发的质量、敏捷性、生产力和速度,本书将展示如何对TDD进行调整,以便在数据库设计与开发工作中获得同样强大的优势。本书共四个部分,全面介绍测试驱动数据库开发(TDDD)技术。

图书摘要

软件开发方法学精选系列

Test-Driven Database Development:Unlocking Agility

测试驱动数据库开发

[美]Max Guernsey.Ⅲ 著

伍斌 译

人民邮电出版社

北京

图书在版编目(CIP)数据

测试驱动数据库开发/(美)格恩齐(Gurnsey,M.)著:伍斌译.--北京:人民邮电出版社,2014.6

(软件开发方法学精选系列)

ISBN 978-7-115-34628-5

Ⅰ.①测… Ⅱ.②伍… Ⅲ.①数据库系统—系统开发 Ⅳ.①TP311.13

中国版本图书馆CIP数据核字(2014)第053387号

内容提要

测试驱动开发(TDD)的实践已经帮助众多软件开发人员提高了软件开发的质量、敏捷性、生产力和速度,本书将展示如何对TDD进行调整,以便在数据库设计与开发工作中获得同样强大的优势。本书共4个部分,全面介绍测试驱动数据库开发(TDDD)技术。第1章至~第4章重点讨论数据库的类的基本概念,第5章至第9章讨论如何用面向对象的方式来精益地做数据库的类的设计以及修复设计的错误,第10章至第13章讨论使用mocking和重构来应对由传统方法开发出来的遗留数据库的两种方法,第14章和第15章讨论如何使一个数据库应用系统能够满足不同客户的不同需求,以及如何将本书的技术运用到其他数据持久化方案之中。

本书适合没有接触过测试驱动开发且正在开发规模较大、需求多变的数据库应用系统的开发人员和架构师阅读,同时也适合尚未在持久化层运用测试先行开发技术的测试驱动开发爱好者阅读。

◆著 [美]Max Guernsey,Ⅲ

译 伍斌

责任编辑 杨海玲

责任印制 彭志环 焦志炜

◆人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京鑫正大印刷有限公司印刷

◆开本:800×1000 1/16

印张:18.25

字数:385千字  2014年6月第1版

印数:1-3000册  2014年6月北京第1次印刷

著作权合同登记号 图字:01-2013-5717号

定价:59.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

广告经营许可证:京崇工商广字第0021号

版权声明

Authorized translation from the English language edition,entitled Test-Driven Database Development:Unlocking Agility,9780321784124 by Max Guernsey,III,published by Pearson Education,Inc.,publishing as Addison-Wesley,Copyright © 2013 by Pearson Education,Inc.

All rights reserved.No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying,recording or by any information storage retrieval system,without permission from Pearson Education,Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD.and POSTS &TELECOM PRESS Copyright © 2014.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

让数据库应用开发不再裸奔

1993 年,我从大学计算机专业毕业后,开启我的IT职业生涯的第一份工作就是在一家国营单位,用dBase III做MIS的数据库应用开发。从那以后,我做了11年的程序员,先后开发和维护过内容管理系统(CMS)、电信运营增值服务系统、通信设备网络管理系统和电子商务系统。这些系统无一例外都使用了像Oracle和MySQL这样的数据库来保存持久化数据。

在这些系统中,有些系统甚至是以数据库的设计为核心来驱动应用程序代码开发的。也就是说,一旦客户的需求到了我们这些程序员手里,先找出需求中的实体(entity),再分析这些实体之间的关系,并画出E-R图(entity-relationship diagram,实体关系图),然后确定各个实体的属性,并找出主键,最后根据这份E-R图在数据库中生成数据库表,之后就可以用像Java这样的编程语言来进行应用系统的开发了。

上面这样看似完美的开发过程,被印在20多年前我在大学修的“关系型数据库管理系统”的教科书上,并多年来一直指导着我做数据库应用开发。但直到最近,我才意识到,这种方法无法解决实际数据库应用开发工作中的最大的难题:一套数据库应用系统交付给相互之间具有不同需求的多个客户时所出现的版本控制问题。回想我所工作的一家软件公司,曾为另一家在国内某领域领先的“巨无霸”甲方公司开发一套以数据库为核心的应用系统。该系统陆续在这家甲方公司的国内8个省得到了部署。俗话说:“龙生九子,各有不同。”这8个省所部署的数据库应用系统虽然都来自最初的那个系统,但是每个省的需求各不相同,这家软件公司只好成立8个庞大团队来维护这8个省的系统。在不堪重负地挣扎了很长一段时间后,这家软件公司下决心要建立一个新系统来统一这8个系统,以减少维护成本。但不幸的是,这个新系统最后也无奈地成为“龙的第九个儿子”。

所幸的是,本书作者Guernsey先生不仅在工作中解决了上述问题,还把解决方案条理清晰地写在了这本书中,功德无量!Guernsey先生洞察到,导致上述问题的原因是各个数据库实例的数据库结构因需求变化而发生变更时,数据库原先保存的数据的版本在此期间没有得到有效的管理。Guernsey先生敏锐地观察到,当发生变更时,面向对象编程中的“类”与数据库开发领域中的“数据库实例的结构”会表现出不同的特点。前者在变更时不需要管理历史信息,只要把运行在服务器上的服务停下来,把旧的“类”替换成新的“类”,再重新启动服务就好了。但是,后者在发生变更时,不可能仅仅把数据库结构替换为新的,而把原有结构中所保存的数据全都清除掉。数据库实例的结构在发生变更时,需要保持以前存储的数据。毕竟,数据库的价值就体现在这些被保存的宝贵的历史数据中。

在洞察到数据库应用开发的上述特点之后,Guernsey先生开创性地把在面向对象编程中得到广泛应用的测试驱动开发(Test-Driven Development,TDD)的理念,引入到数据库应用开发——这个几乎还处于类似面向机器编码的“汇编语言”时代的蛮荒之地,就像本书英文版封面所展示的那样,在一片广袤的沙漠中,赫然出现了一片环绕一眼清泉的生机勃勃的绿洲。在把TDD的理念与在数据库应用开发时保存历史数据这个特点相结合后,就如同给以前无奈地进行“裸奔”的传统数据库应用开发的程序员们穿上了“测试”这层坚实的铠甲。

Guernsey先生不仅讨论了传统关系型数据库的测试驱动开发方法和技术,还在本书最后一章将这些方法和技术运用到XML应用、文件系统及其他对象目录和序列化数据对象之上。相信本书所阐述的测试驱动数据库开发的概念和原则,都能适用于任何需要对数据进行各种形式的持久化应用系统的测试驱动开发之上。

Guernsey先生的上述开创性的工作使我决定暂时中断写自己的《驯服烂代码》一书,而花费4个月中全部的空闲时间来翻译本书。我在翻译本书时,力求用通顺的语句来表达作者的原意,争取让自己阅读翻译后的文字就如同喜欢阅读原文一样。如有翻译不当之处,恳请通过我运营的“北京设计模式学习组”的微信公众号bjdp.org给予指点,以求改进。我个人也会在该微信公众号中创建和维护本书中文版的勘误表,以方便读者。

本书的翻译工作得以在4个月内顺利完成,离不开我妻子薛静、儿子乐乐、岳母大人及其他亲属的理解和支持;离不开我20年前的大学同学杨光从远在万里的大洋彼岸给我的有关美国“对冲基金”公司运作模式的专业介绍;离不开人民邮电出版社杨海玲编辑对我的信任;离不开我的微信、微博和北京设计模式学习组(bjdp.org)这些圈子里各位亲友的关心;离不开我的父母的无私养育之恩和点滴帮助之举。

最后,希望Guernsey先生所带来的测试驱动数据库开发的理念,能为在中文世界里辛苦加班的程序员带来能够化解数据库应用开发的种种烦恼的一剂良药。

——伍斌,

独立匠艺程序员,公益免费编程操练社区“bjdp.org北京设计模式学习组”创办者

我是一位具有多年测试驱动开发(Test-Driven Development, TDD)经验的实践者,并且以编写、讲授和教授TDD的一些课程作为我在Net Objectives软件咨询公司的部分工作职责。在课堂上讲课或在技术大会做讲座时,我经常被问及这样一个问题:TDD对于解决业务逻辑层和其他“中间层”(middle-tier)的问题似乎很完美,但是对于表示层(UI)和持久化层(数据库)又如何呢?

我的回答通常是:对于上面提到的表示层和持久化层,都分别存在下述两个问题。

1.如何管理中间层对其他两个层的依赖关系,从而使中间层的行为更加容易测试?

2.如何测试驱动其他两个层?

第一个问题大体上与接口定义、mock对象、设计模式和良好的关注点分离相关,这是有关技术的问题。对于将业务逻辑从其依赖的对象中分离出来,TDD社区已经有许多成熟的、经过验证的技术。我的课程的大量时间都花在传授这些“技巧”上了。

但是对于第二个问题——测试驱动用户界面和数据库开发——我总是说这些在很大程度上是尚未解决的问题。这并不是说我们不知道如何测试用户界面和数据库,而是我们不知道如何用测试驱动它们,不知道如何根据TDD的要求编写分离、快速和合适粒度的测试代码。面对系统中上述其他两个层时,我们不得不满足于更加传统的测试。这一直是我对上述问题一贯的回答。

据我所知,对于UI,测试驱动问题依然是尚未解决的。但是对于数据库,Max Guernsey已经找到了解决测试驱动问题的方法。

在我的《浮现式设计》一书中,我谈到了许多系统设计和自然界进化过程的相似性问题。如果你把自己的源代码想象成系统的“DNA”,把可执行文件想象成“生物个体”,那么这个比喻真的很恰当。DNA用来生成个体。要改变物种,首先DNA要发生改变,然后下一个生成的个体才会改变。大自然不进化个体,但是大自然让每一代的物种都发生变化。这与软件的编码/编译/运行的特点很相似。我们抛掉.exe文件,改变源代码,然后编译器就产生了一个新的、不同的、希望能更好的可运行文件。正因为如此,源代码为王。源代码是我们不可或缺的。

Max对上述问题的认识源自于他所认为的数据库不同于源代码的观点,这一点至少从我与Max的谈话中就可以看出。如果把数据库schema比作DNA,那么一个安装好的正在运行的数据库实例(存储了所有企业的关键数据)就好比一个个体,这时我们就不能再用大自然的比喻了。我们能很容易地重新创建数据库schema——只要运行DDL[1]脚本或其他等价代码就可以了。但是一个给定的、安装好的、“活”的数据库包含的信息和知识,在数据库结构发生改变时必须保持完好。

因为上述原因,进化的范型(paradigm)就不适用了。在数据库中,个体是至关重要的。我们不能简单地抛弃个体,然后用一个改变了的DNA来重建。这再一次表明,大自然不会进化个体。但是大自然中还存在另一种范型能够解释数据库的问题,那就是变形(morphing),这正是一个生物个体从一种生命阶段转变到另一种阶段的方式,如蝌蚪变成了青蛙。在研究了上述认识及由此引发的所有结果后,Max发展了一种审视数据库的真正革命性的视角:如何创建、改变并最终测试驱动数据库开发。他将 TDD 实践者所喜爱的清晰、安全和有效的开发方法传授给了数据库开发实践者。

本书是一部开创性的著作,Max发现了数据库开发的“罗塞塔石碑[2]”(Rosetta Stone)。当你在面对系统的持久化层时,如果能认真地跟随他,你将踏上一条能有效完成工作任务的康庄大道,你将获得知识、工具和整体方案,从而让测试驱动数据库开发成为可能。

——Scott Bain

Net Objectives软件咨询公司高级咨询师

[1].DDL,Data Defination Language或Data Description Language,即数据定义语言或数据描述语言,是一种语法类似于计算机编程语言的脚本,用来定义数据结构,特别是数据库schema。引自维基百科。——译者注

[2].罗塞塔石碑(Rosetta Stone,又译为罗塞达碑),是一块制作于公元前196年的大理石石碑,原本是一块刻有古埃及法老托勒密五世(Ptolemy V)诏书的石碑。由于这块石碑同时刻有同一段内容的三种不同语言版本,使得近代的考古学家有机会对照各语言版本的内容后,解读出已经失传千余年的埃及象形文之意义与结构,从而成为今日研究古埃及历史的重要里程碑。引自维基百科。——译者注

前言

本书讲述如何将测试驱动开发的概念应用于数据库开发。

谁应该阅读本书

这个问题的简短回答是:“任何想要学习如何对数据库进行测试驱动开发,并且甘愿为此辛苦工作的人。”详细的回答见下文。

本书主要是针对这样的程序员,他们以某种方式负责基于至少一个数据库设计的开发工作,其次是针对那些认为自己主要是做数据库开发,并且对在其开发流程中加入测试驱动开发有兴趣的人。

这绝对不是削弱上面第二组人的价值。在写本书时,本书中涉及的技术是构筑在获得上述第一组人广泛接受的原则和方法的基础之上的,而且仍然努力地从第二组人那里获取推动力。这不是说事情就不会发生变化,我希望会,但是如果我试图仅仅关注从第一组人那里获取技术的原则的话,本书就可能会失去平衡。

本书的目标是帮助人们将测试驱动开发过程运用到数据库开发的新领域,在这些新领域中,施加在TDD上的影响力会多多少少与其他领域有所不同。

如果你阅读了本书,并能够持续地通过测试来驱动自己的数据库的开发,那将是双赢的结果。如果你开始使用这些原则来将其他的技术移植过来,比如面向模式(pattern-oriented)的开发,那么你就会得到双倍的效果。如果你开始将学到的原则移植到涉及长期保存数据的其他领域,如系统安装程序,那么你会有更多收获。

需要做什么

为了回答这个问题,我从测试驱动开发为什么能够解决问题讲起,然后再看为什么想要在数据库世界获取测试驱动开发的推动力会有那么多麻烦。注意,我不是认为数据库开发基本上是未经测试的,只是根据我的经验判断,数据库开发既没有达到持续测试,也没有达到测试驱动的程度。

使数据库测试变得很困难的主要原因是类(class)概念的缺失和错位,甚至是最“狂野的西部风格”(Wild-West-style)的现代编程语言也支持类与实例的概念。

数据库引擎要么通过为数据结构提供类来做些小恩小惠的工作,要么就干脆对建立真正可以测试的类完全无所作为。对我来说,出现上述问题的原因是,人们通常没有认识到什么是数据库领域真正的头等公民对象(first-class object)——数据库本身。所以,第一步是建立一个数据库对象。

变化是测试驱动开发过程的核心。为了支持新的需求和一个不断扩充的产品特性集合的可测试性,需要经常改变设计。一个使测试驱动开发难以被采用和支持的影响力是:与在其他种类的设计中相比,变化在数据库设计中,被认为更加危险。

如果在你的中间层中搞乱了一个设计的变化,就必须回滚。如果你在数据层搞乱了一个设计的变化,就得删除存储在其中的有价值的知识。解决方案是不仅测试你的数据库做了什么,还要测试数据库是如何变化的。

另一个在改变数据库设计时需要面对的问题是,数据库与其客户端之间的耦合是弱遵循的(weakly enforced)[1]。可能存在这种情况,即在修改了数据库接口的同时,却没有意识到你已经将下游应用程序也搞坏了,并使该状况持续了很长时间。通过运用数据库的类来固化数据库设计与客户端的关联关系,能够缓解上述风险。

通过创建对变化进行有效控制的强大的数据库的类,解决了数据库世界中阻碍TDD应用的基本问题,就好比在支持现代软件开发实践方面,把开发人员带入了20世纪90年代早期。

现在已经进入了21世纪,你必须得走得更远一点。为此,我会帮助你理解,测试的范围应该是软件行为的验证。我会针对数据库开发来定义什么是软件行为。

使浮现式设计成为可能

我也会展示如何将长期的可维护性最大化,这可以通过将数据库的范围限定到满足你当前的需求,并使用本书描述的技术来使将来能容易地增加更多的产品特性,也就是说,我将帮助你消除因为没有提前做数据库设计规划而产生的恐惧。

没有一个过程是完美的,即使有完美的过程,执行过程的人也没有一个是完美的。尽管你尽了所有努力去避免错误,但是错误总会发生。如果你在做真正的测试驱动数据库开发,很多时候错误会以下面这种形式表现出来,即某种行为没有在你的测试套件(test suite)中被表达出来。我将展示如何最佳地修正错误。

了解了如何为数据库的类开发和编写测试,同时保证数据库的设计尽量简单和贴切(problem-appropriate),将会把数据库开发带入21世纪,从而让数据库开发仅仅落后于现代面向对象开发一个短暂的时代。

开发现代化

支持测试驱动过程的数据库开发现代化的最终阶段,包含了采用和适应我称之为“高级的”面向对象的一套方法。

掌握“类/数据库”级别的设计是第一阶段。如果可以在一个大数据库的设计和两个小数据库的设计之间进行选择,你应该选择那两个小数据库的设计。如果数据库技术不允许,那么使用组合(composition)方法来将两个逻辑的数据库实例置于一个物理的数据库实例中。

另一个重要的行为是重构。你需要保持数据库的设计在数据库的整个生命周期中对所涉及的问题都十分贴切。这意味着设计一定要从小的设计开始,当你的设计包含了越来越多的问题领域的内容,设计也需要跟着改变样子。我将为你展示如何在测试驱动过程中重构数据库设计。

这些就是你为一个典型和崭新的数据库设计做测试驱动数据库开发所需要做的所有事情。本书其余部分是专门帮助你对付那些出生在一个没有测试的环境中、偏离了被控制和可测试轨道、用非面向数据库应用开发过程进行开发的数据库,并与之进行“缠斗”。

做测试驱动数据库开发并不容易,尤其是开始的时候。如果你已经牢固掌握了“正规的”测试驱动开发方法,那么做测试驱动数据库开发就完全没有什么令人惊讶的感觉。然而,不管你学习新技能的速度有多快,学习TDD会比其他技能花费更长的时间。

花费更长的时间意味着带来更高的价值。读完本书,你将对测试驱动数据库开发有一个理论上的理解。1个月、3个月甚至18个月之后,你将精通这门技能。

当精通这门技能之后,你将能够满怀信心地频繁、快速和安全地改变自己的数据库设计,能够在恰好你需要的时候构建恰好是你需要的系统。最终的结果会使数据库的改进能够成为软件开发流程中一个运转流畅的环节。另外,你将能够保持自己的数据库设计整洁、简单和快速。

逐章内容简介

下面对本书章节逐一做一下介绍。

在第1章中,我解释为什么我写这本书,谁应该阅读这本书,什么是在做数据库设计时挡住通往TDD的拦路虎。我之所以写本书,是因为真正的测试驱动开发没有从数据库领域中真正地获得任何推动力。本书的目标读者是那些认为自己是软件开发人员,并且在工作中必须与数据库设计打交道的人。在数据库开发领域中,最大的问题是不存在一个类的清晰的概念,而类是传统的TDD开发中的核心元素。

为了构建一个数据库的类,需要把那些在数据库上运行的脚本原封不动地作为永久记录保存起来,并用一个清晰的方式跟踪哪一条脚本已经运行。用基础设施工具软件能够确保每一个数据库的类的实例是用完全相同的方式创建出来的。在第2章中,我将展示如何能做到这一点。

为数据库设计实施可持续操作的TDD过程会涉及许多步骤。第一步是定义一个基本的TDD过程,以便于在开发后期能在此基础上添加更深入、更面向数据的行为。在第3章中,我会展示如何针对一个数据库的类做一些简单的测试驱动开发。

在第4章中,我将展示如何克服一个很大的障碍:与变化相关联的风险。频繁地引入变化会让很多人心生恐惧。令人恐惧的根源是数据库存储了大量有价值的数据,因为匆忙地做出变更而造成的数据丢失在大部分情况下是不可接受的。如果你不仅能测试数据库的行为,而且能测试构建和修改数据库的脚本,那么就能够征服恐惧和风险。

在软件产业中,数据库是最被其他子系统所依赖的子系统,并且对一个数据库设计的修改可能会造成不可预知的后果。这个问题的核心是许多大规模的易传播的重复数据被简单地看做“自然的”事情而被人接受了。在第5章中,我将展示如何通过消除重复数据来控制一个快速演进的数据库设计的成本。

从TDD过程的角度来看,测试在对象中规定了行为。于是,问题就变成“在数据库中什么是行为?”在第6章中,我通过回答这个问题来为测试定一个良好的范围。

一旦单个测试的范围被定义好了,就能获得探索更大的主题的自由,即探知哪一种数据库设计对变化有利,而哪一种很难维护。与试图预测几个月之后的需求相比,在第7章中我会展示让数据库保持轻量、精益和简单是一条更好地支持未来需求的道路。

“对,如果你从不把事情搞糟,这一切都很棒。”有人可能会说,“但如果我们把事情搞糟了该怎么办?”在第8章中,我会展示一些技术,这些技术能让你处理任何可能影响你的数据库设计的计划外变化。

在第9章中,我针对如何设计一个能将可测试性最大化的数据库的类,提出一些建议。然后进一步展示如何在数据库的类中应用面向对象的设计概念。

测试经常被不必要的耦合所困扰。行为之间的依赖能产生涟漪效应(ripple effects),一个单独的变化,能引发几十个测试失败。在第10章中,我会展示如何运用第9章讨论的设计技术来将不同的行为彼此分离。

测试覆盖率越高,引入变化的速度越快,修改设计的频率也会越高。在第11章中,我演示了如何在保持行为不变的情况下改变数据库设计。

一个过程不能算作完备,除非它包含了能够处理在该过程引入之前就已开发了的软件的机制。在第12章中,我讨论了两种方法之中的其中一种,逐步用测试保护那些没有使用本书提及的实践方法进行开发的数据库。

第13章讨论了处理遗留数据库的另一种方法。当采用Façade模式时,可以将一个遗留设计封装到一个经过良好测试的新设计之内,然后逐步地将行为从旧的设计转移到新的设计中。

我不会将本书提到的方法作为解决将TDD带入数据库开发领域而引发的问题的“一刀切的”解决方案来进行兜售。不加修改地运用本书提到的实践,对于很多人来说都是有成效的。然而,有些人操作这些实践的前提条件无法与本书前13章的内容所涉及的条件完全符合。在第14章[2]中,我讨论了一些我见过的人们过去在实施过程中采取的一些变通做法。

最后,在第15章中,我演示了将本书讨论的各种技术运用到除数据库之外的其他数据持久化方案的一些方法。这些其他存储机制的例子有文件系统、XML文件和令人恐惧的序列化中间层对象。

代码下载

本书用到的代码可以访问以下链接下载:http://maxthe3rd.com/test-driven-database-development/ code.aspx

[1].即遵循数据库的接口,参见第5章。——译者注

[2].第14章英文标题Variations除了有“变异”的含义外,还有“变奏曲”的意思,如此翻译是用音乐术语“变奏曲”来比喻测试驱动数据库开发的一些变通的做法。根据英文版《牛津高阶英语词典》(Oxford Advanced Learner’s Dictionary)的解释,变奏曲指:根据一个简单的曲调不断以不同的和更加复杂的形式进行重复的一组简短的乐章。——译者注

作者介绍

Max Guernsey目前是Hexagon软件有限公司的管理成员,他有15年的专业软件开发经验,这期间的几乎一半时间,他一直就敏捷和测试驱动数据库开发主题,写博客、写作和发表演讲。

在Max的职业生涯的大部分时间里,他作为咨询师,使用多种编程和数据库技术,为各种不同行业的软件公司提供指导。在上述大部分工作中,他花费数月甚至数年的时间帮助团队运用像测试驱动开发、面向对象的设计、验收测试驱动开发和敏捷规划这样前沿的技术。

Max总是作为一位“实操型”的顾问和团队一起长期工作,以帮助他们构建软件和技能。这一系列多样且深入的工作,帮助他获得了那些阻碍大多数敏捷团队的、与数据库相关的测试和设计的问题的独特理解。从2005年以来,他一直在思考、写作、写博客、演讲和创建面向开发人员的软件,以解决上述问题。

读者可以通过Max的邮箱 max@hexsw.com 与他联系。他也会定期在他的 Twitter(@MaxGuernseyⅢ)和博客(maxg3prog.blogspot.com)上发表文章。

致谢

本书的背后有众多人几乎跨越10年的帮助。

首先感谢我的妻子Amy,在本书创作的过程中,她一直是本书的动力和校验的源泉。在我们一起度过的15年中,她几乎校对了我写的每一段文字。

Bill Zietzke在本书创作的初期提供了帮助,正是我和他之间的一次谈话,引发了本书的创作灵感,当时我们二人在位于华盛顿州贝勒维市的一家保险公司做软件开发。

Beau Bender协助发现了我设计的用于控制数据库和客户端之间耦合关系的机制,为此我感谢他。

同时也感谢我的好朋友和导师Scott L.Bain,正是他最先鼓励我写作并出版图书的,也正是他的影响使得我达到了这个目的。在这一方面,他一直通过提出有价值的问题、批评和观察来扮演一个重要的角色。

在我职业生涯的主要时间里,Alan Shalloway也是我的一位朋友和导师,他帮助我判断我的第一个想法——向所有人传授所有事情——是不正确的。我确信,如果没有他非常有建设性的批评,你或许正在读一本完全不同的书,并可能由一个完全不同的作者所写。

Alan和Scott在我最近作为专业软件开发者的开发工作中扮演了关键性的角色。他们之中每一位都能看到我所具备的技能,并帮助我继续发展这些技能,同时能用一种避免我对新的事物和想法产生抵触的方式,为我提供我所欠缺的技能。

他们对于如何与人相处的建议给我带来了同样甚至更大的价值。在撰写本书的时候,我还不完全是一个受人欢迎的、容易促成共识的人,但是与遇到Scott和Alan之前相比,我在说服人的方面已经有了很大的改善。如果没有他们的指导,没有他们向我展示用易于接受的形式来分享我们的知识是多么的重要,我可能开始的时候不会把写书的事放在心上。

我最近结交的一些朋友和同事甘愿充当“小豚鼠”,为我试读了本书各章的早期版本。这让我能够很快获得反馈而采取行动,并帮我决定对本书的表现形式进行第二次修改。如果没有Seth McCarthy和Michael Gordon Brown的反馈,我就会试图撰写一部更厚的有关“敏捷”数据库开发的书,而不是你现在正在阅读的这本更加有专注性和技术性的书。

不言而喻,我父母也为本书的撰写承担了部分责任,因为如果没有他们,也就没有我。然而,在我还是年轻程序员进行软件开发的时候,我父亲扮演了特殊的角色,如果没有他的影响,或许我就像数学家或华尔街分析师那样没有用处。

第1章 为何改变书的内容、谁是目标读者和什么是障碍

本书主要介绍如何将测试驱动开发(TDD)法则有效地运用在与数据库功能交付相关的任务上,是专为那些想知道如何将测试驱动开发法则运用到数据库编程上的人们而写的。然而,在讨论如何做之前,需要先看看为何改变本书内容,谁是目标读者,以及什么是障碍。

1.1 为何改变书的内容

本书最初的书名是《敏捷数据库开发:从需求到交付》。到现在书名和内容都己经变了好几次。

在读者反馈和自我启发的双重作用下,本书经历了几次激进的蜕变,由开始的一本主要讨论开发过程并稍带讲一点技术的书,转化为讨论 TDD 的差异如何影响敏捷数据库开发过程的书,然后最终将有关团队级别的开发过程内容完全去除而成为今天这个样子的书。

为什么我几次变化本书的方向?主要是因为开发人员学到的有关敏捷软件开发的几乎所有有关过程管理(process-management)的知识,对任何领域都是普遍适用的,通常情况下甚至适用于硬件设计。我过去写的很多东西,要么是对别人写过的内容的重复,要么就是向人们说教那些他们已经接受了的东西,再写这些东西注定都是无效的。对于那些在构建数据库时寻求如何做敏捷开发的建议的人,建议先学如何做敏捷软件开发,然后再将其运用在数据库上。

当然,如果你做数据库开发,你会发现你需要学习如何做TDD。要想让任何敏捷开发的努力取得成功,测试驱动开发的法则是绝对至关重要的,并且我相信是无可争议的,你需要一系列可执行的规格说明来告诉你,你所做的变化是否安全。如果没有这样的规格说明,你就不可能让敏捷有成效。

事情在这里变得有些棘手,将测试驱动开发运用到传统面向对象开发和数据库开发中会有很大的不同。也就是说,尽管 TDD 的原则普遍适用,但是为构建应用程序和中间层逻辑而开发的相应的实践,却不总会完美地被转换到数据库领域中。

另外,即使你的团队还没有准备好拥抱敏捷软件开发,你也能使用测试驱动开发。

1.1.1 每天敏捷都在逐步地入侵我们的领域

文明的潮汐控制着人们做事情的方式。当这个大潮退去时,我们集中起来建立指挥和控制结构,并试图在大批量和排长队的现象中找到效率。当潮水涨回来时,我们又打破那些结构,开始将控制权交到“平常”人的手中,并且试图在那些离问题最近的人所快速做出的决定中找到效率。

潮水正在涨回来,在本书写作期间,潮水也将可能退去。

对于我们来说,潮水涨潮是否正确,就跟站在海滩上的人关心水冲到岸上是否正确一样,是无关紧要的。波浪已经来临,不管你是否喜欢,最好做好准备。因为开发人员不得不用更小的增量和更快速地交付价值的方式来改善开发工作,因为人们已经不愿意经过漫长的等待才能得到自己想要的东西。

一些人认为数据库是个例外,是一种特殊情况,即能够忽略那种驱动力,该驱动力无处不在,并促使我们更频繁地做出改变和对实际需求进行反应。没有更客气的方式来表达这个意思:那些人错了。数据库也不例外。当然,数据库开发者或许可以躲在海湾里将原来的开发领域保留得比其他产品开发领域更长一点,但是并不能阻止潮汐。在未来的几年或几十年里,快速做出变化的压力将变为压倒性的,并且坦率地说,与能适应该趋势的人相比,那些阻止这种趋势的人最终将被降级为不那么重要的角色。

1.1.2 若没有TDD敏捷就没有成效

试图得到快速的工作交付仅存在一个问题:变化是件危险的事情。如果你正在做交付价值的事情,那么对这件事所做的变更必然会危及价值的持续交付。如果你的产品停止交付价值,那么你就会开始失去与客户的良好关系。如果任由上述腐化持续足够长的时间,你就会开始失去客户。

除了那些把股票借给对冲基金公司来做“放空”操作的托管代理机构,以及做类似事情的组织之外,没有人想要失去客户[1]。所以,即使变化发生得非常频繁,保护价值的机制也必须落实到位,这样的机制才允许我们能够很快地做出改变。

上述机制就是测试驱动开发。测试驱动开发提供了多种优势,但对于本书所讨论的内容来说,其主要优势是用自动测试为软件组件提供充足和有意义的保护。部署到位的测试一方面能够在破坏了某些代码时立即给你反馈,另一方面也能防止你把某些已处于被破坏状态的代码作为产品发布出来,这两方面的优势缺一不可。

除了上述测试驱动开发最关键的好处之外,还有许多其他好处被很多人奉为 TDD 的“主要的”好处,包括加强软件设计分析或者戏剧性地减少过度构建。上述好处非常重要,本书将讨论这些好处,但是它们不会让你的开发工作做得很快,它们仅仅是帮你把工作做得快一点而已。

1.1.3 在数据库领域运用TDD是个挑战

本书之所以存在,是因为 TDD 非常难以在数据库开发中得到运用,特别是难以用一种能够让开发者快速地对变更进行开发、验证和发布的方式来运用。

这种困难很大程度上源于开发人员最初构建数据库的方式。在许多组织中,数据库是远古遗迹。一些特定的数据库实例非常重要,且其重要性已经让设计本身黯然失色。

我见过许多这样的案例,开发者只基于一些极其重要的实例来考虑数据库的设计而不顾其他。也就是说,他们只考虑设计“这个”开发数据库、“这个”测试数据库和“这个”生产数据库(production database),并且他们主要关心如何将变化从一个数据库传播到另一个数据库。为了使用测试驱动开发进行数据库设计,这种思考方式必须做出180°的大转变。

本书接下来的各个章节将解释如何能做到这一点。

1.2 谁是目标读者

在讨论问题真正的本质是什么和如何解决问题之前,先谈一下关于本书的目标读者。任何读者都可能从本书获取价值,但在使用这本书之前,读者需要具备一些必备的技能。

1.2.1 TDD和OOP

为了运用好本书中涉及的技术,读者需要理解测试驱动开发和面向对象编程的好处,但不必是这两方面的专家,只需要知道实现上述两个方面之后所带来的好处是什么。下面这些论断是有意义的。

•TDD通过让开发者保持在正确的轨道上,以使你能够快速地开发。

•OOP通过把不相关的事物进行封装并彼此分离,以使开发者能够快速地开发。

如果上述论断你认为是正确的,那么你就具备了良好的知识。

1.2.2 应用程序和数据库

接下来读者需要具备的是对于应用程序是如何与数据库进行交互的概念和对于数据库是如何工作的理解。如果读者从未写过一行应用程序代码,则需要找一本C#、Java、C++、Ruby或JavaScript的书,自己学习一下如何写一些简单的算法和定义一些基本的接口。

1.3 什么是障碍

那么,真正的问题是什么呢?是什么真正阻碍了测试驱动数据库开发的实现?从根本上讲,上面这个问题的答案就是数据库——单独的服务器和数据库实例,即持久化解决方案的运行平台。与一个用于创建一组最终能够被装载到任何特定会话的应用程序的二进制文件的设计相比,一个单独的数据库更像是一个包含JVM和一个应用程序的特定会话的进程。

开发人员需要摆脱不良的做法,转向构建不再绑定任何特定数据库实例的数据库设计。

1.3.1 数据库就是对象

数据库就是对象,从面向对象编程的程序员的角度看,它们是长期存在的对象,即便如此,它们仍然是对象。现如今,许多人对待数据库的方式类似于软件开发早期计算机程序员针对计算机本身编程,而不是针对那些能够运行在计算机上的程序编程。

除了一些遗留系统,上述开发应用程序代码的方式因为一些原因而像渡渡鸟[2]那样绝迹了。最明显的原因是现代计算机环境的复杂性要求能随时随地新增或移除硬件。另一个原因是很多应用程序不得不运行在多种多样的硬件上,或者运行在物理上与开发软件的程序员分离的环境中。

1.3.2 TDD适用于类,不适用于对象

对于数据库领域中的 TDD 来说,其最大的障碍存在于测试的内在本质中。测试天生就是在对象之上进行操作的。当一个人运行手工测试时,他把大量的对象作为接口并与之打交道,包括正使用的应用程序的一个实例和与之交互的大量业务逻辑对象的实例,这些实例根据它们的输入和/或输入产生的结果来作出决定。

其他产业并不是像上面描述的那样做测试。生产需要长期使用的绝对关键产品的公司,如为替换受损髋关节而生产人工髋关节的公司,是需要测试从生产线下来的每一件产品的。而生产那些不是长期使用或不大可能因为产品失效而造成严重人身伤害的产品的公司,如生产铅笔的公司就通过测试产品在统计学领域的显著部分来近似做到测试他们生产的每一件产品。

为什么软件开发人员能够不用测试每一件产品,而仅仅测试他们所做的一些实例呢?

在软件产业,能够像这样做测试可能是因为同一个类的两个对象能做出完全相同的行为。你能用一个类产生任意数量的对象,所有这些对象都具有完全相同的行为。

软件开发者进行开发的这一独特特性,允许一个类的任一实例能作为该类其他每一个实例的模板。这意味着,仅仅对一个对象进行测试就能获得这些对象的类的情况,并能获得该类已经和即将创建的所有对象的情况。

然而,如果开发人员所处的情况与上述情况不同该怎么办?如果一个类的每一个实例都是通过手工或者通过在另一台计算机上的一个不可靠的过程组装成的,那该怎么办?在这种情况下,测试一个对象就不能获得另一个对象的情况。开发者必须测试所构建系统的有统计学代表性的一个子集,而需要做的测试数量将与可以接受的风险数量相匹配。

这就是在数据库世界中开发者所面临的处境。开发者开发了一个设计,很少能有一个容易的方法来将该设计的变化引入到新的或现存的数据库实例中。更多的时候,开发者会在某类数据库的所有各种重要的实例上手工调节设计的变化,这等同于在每一台需要作出更新的计算机上手工修改和检查汇编代码(或者是一些非常初级的源代码),然后在那台计算机上重新编译二进制代码。

当开发者以上述方式开发时,就近乎于需要测试所创建的每一个产品实例。然而,在软件产业,测试通常需要对所测试的对象大动干戈,而开发者不能让产品实例上的实时数据遭受测试带来的那种危险。一些数据库实例,特别是生产数据库,根本就无法进行测试。

1.3.3 我们需要数据库的类

如果开发者需要了解一个数据库的情况,但是不允许对其进行测试,就需要找到一种方法,通过测试一个代理来了解那个数据库是正常工作的。正如本书已经提到的,在应用程序开发世界中允许我们做此事的机制就是类。

因此,测试驱动数据库开发的基础就是建立数据库的类,而不是建立特定数据库的实例。定义的类负责构建和更新测试实例。在从测试实例中获得足够多的反馈来验证对一个类所做的变化之后,接下来该类就以更新测试实例完全相同的方式来更新生产数据库。

相对于那些开发者不想运行任何测试的产品实例来说,通过这种方式可以确保测试实例是个良好的模板。在本书的整个内容中,读者将看到同样的过程不仅允许检查对数据库的修改做了什么,还能检查修改是如何被引入的。当开发者想要把一个数据库的修改发布到任何一个生产环境中时,你完全有信心相信上述修改“是能够工作”的。

1.4 小结

在写本书时,精益和敏捷软件开发正在全球大行其道,但是数据库开发在开发过程中仍然是瓶颈。为了消除阻挡整个组织的障碍,开发人员必须找到如何让数据库快速变化的方法。

然而,不能以牺牲对变化的信心来换取快速的变化,数据库是如此重要,以至于不能容许有任何额外的风险。这里 TDD 就可以派上用场了,与那些没有使用TDD缓慢的最小心翼翼地做出的变化相比,TDD能让开发者冒更小的风险而做出更频繁、快速的变化。

即使这样,开发者还是不能简单地将已经学到的适用于“常规”软件开发的技术不经修改地加以运用。开发者必须考虑数据库领域中出现的那些不同的影响力,而这正是接下来的章节中要介绍的内容。

下一章将逐一讨论建立一个数据库的类而需要完成的步骤。

[1].在美国,一些大型投资基金机构购买股票后,一般将其托管在由一些大的银行或券商组成的托管代理机构手里。另外,一些对冲基金公司可以从上述托管代理机构手中借到股票来进行“放空”操作,为此对冲基金公司需要向托管代理机构支付利息。对于这些托管代理机构,一方面其客户不会知道它们可以把卖家已经卖出的股票再借给对冲基金公司来做“放空”操作,另一方面其手中持有的上述这些用于做“放空”的股票十分稀缺,所以这些托管代理机构不用担心失去客户。——译者注

[2].渡渡鸟,或作嘟嘟鸟(Dodo),又称毛里求斯渡渡鸟、愚鸠、孤鸽,是仅产于印度洋毛里求斯岛上一种不会飞的鸟。在被人类发现后仅仅200年的时间里,这种鸟便由于人类的捕杀和人类活动的影响彻底绝灭,堪称是除恐龙之外最著名的已灭绝动物之一。引自百度百科。——译者注

第2章 建立数据库的类

开始测试驱动数据库时,需要做的第一件事是定义数据库的类,并且不用过多地担心特定的数据库实例。读完本书后,读者将有可能开始从允许任意的手工修改,转变到允许保持任意有意义的数据库实例。为了帮读者达到这个目的,本章将深入讨论什么是类以及类如何能够提供帮助,还将深入探讨在数据库开发中的影响力是如何不同于应用程序开发的影响力的。

在调和了类的本质与在数据库开发中出现的新的影响力之后,本章展现了一个数据库的类的需求,并展示了如何实现该需求。希望能为开发者提供与实现相分离的需求,能够让开发者更轻松地得到本章的输出结果,同时能适应开发者的开发环境。

2.1 TDD中类的角色

在测试驱动开发中,一个类的主要作用是提供一种机制,以便许多具有相同行为的对象能够被创建。这一点非常重要,因为测试软件的方式就是通过检查一个单独对象的行为,并据此来预知从该对象的类生成的所有其他实例的行为。

当没有类时,测试仅仅告诉开发者有关某个特定对象的情况。当有了类时,测试会告诉开发者有关对象将如何被创建的情况,并进一步告诉开发者所有其他对象将如何被创建的情况。

2.1.1 可靠的实例化过程

当人们说“我写了一个对象来做X事情”时,事实上并没有写一个对象,而是写了一个类的对象来做X事情,正确的说法应该是“我写了一个类来做X事情”。

人们说他们写对象是因为类和对象之间的界限对他们现在来说是非常模糊的。有一个类通常意味着能得到一个对象,能够得到一个对象通常意味着有一个类。

如果用C#来写一个类的对象,实例化甚至不是要考虑的。这缘于开发了这个类,就意味着它将完全可靠地提供所需要的任意数量的对象,这些对象都能被创建,都能工作,并且都能以该类的其他实例完全相同的方式来工作。

2.1.2 测试检查对象

类能够安全和一致地产生对象这件事是如此的基础,以至于开发人员完全把其当成理所当然的事情。当教一个应用开发人员做测试驱动开发时,我可能会让他为构造方法写一些测试,但是只让他测试需要增加到这个构造方法上的行为,而不需测试刚刚创建好的对象是否被正确地创建了。

从小的方面来讲,那是因为我不认为构造方法比测试一个方法调用更有测试的必要。然而,这不是我不教人测试构造方法进行对象构造的主要原因。

我不教人测试构造方法进行对象构造的真正原因,是因为对我来说从来就没发生过构造方法可能没有构造好一个对象这种情况。当然,这种情况发生的机会还是存在的,但是微乎其微,本书不再考虑这个问题。

以上是对概念进行的一些解释,读者需要换一种方式来考虑这个问题。当在面向对象的系统上进行测试驱动开发时,我实际上是用类之间的关系来进行思考。测试类测试生产类,生产类使用服务类,等等。

然而,实际情况并不是这样的。当为一个生产类写一个测试类时,该测试类真的没有测试该生产类。取而代之的是,该测试类产生了一个内含一堆测试的测试对象。这些在测试对象里的测试接着就获得生产类的实例,并请求这些实例来做事情,然后对结果进行分析和报告。

通常情况下,尽管每次运行都使用全新的测试和生产对象,但都可以数百次地运行同样的测试,并看到完全相同的结果。正是这种可重复性让我确信,在生产部署环境中,生产对象将会正常工作。事实上,上述可预见性允许用类之间的关系来思考,而不是用它们创建的对象之间的关系来思考。

2.2 面向对象编程语言中的类

为何对象的类来到应用开发世界的时间要远远比数据库的类早呢?首先,与在应用开发世界相比,在数据库世界中能让类成为必要元素的影响力没有那么强大,这一点先暂且不谈。其次,相比创建数据库实例,我们能够更加容易地建立可靠的方法来在应用会话中创建对象。

2.2.1 类的构建很容易:构建新对象即可

在面向对象编程的世界中,类其实仅有两个职责:创建新对象和析构(destroy)被废弃的对象。就本书的目的而言,析构其实并不重要。然而,对象的创建绝对是重要的。

在像C#、Java或Ruby语言中,当一个类被请求构建一个新的对象时,该构建过程始于一个空的内存块。此时该内存块里保存的内容与该对象毫不相干,所以该类能够直接用构建该对象所需要的内容覆盖之。在某些情况下,此事就像下面这些操作那样简单,即在虚方法表中添加指针,将余下的字节清零,并委托构造方法进行进一步的初始化操作。在另外一些情况下,此事又像下面这个操作那样复杂,即构建结构化数据来指向各种元数据、方法等。

然而,无论如何,此事是如此的复杂,以至于我们不能用一些简单的数学规则在开发人员对设计的表达与创建一个类型的实例所需的精确的步骤集合之间进行转换。

2.2.2 一条途径:必要时析构

对于一个类,开发人员可能会做下面一些事情。这些事情中有许多是与设计有关的。例如,使用测试先行(test-first)的技术从零开始开发一个类;使用现有的测试来重写一个类;修改一个类的测试套件(suite of tests)来改变类的行为。

有些需要在类上做的事情与部署有关,这会产生两种主要的事情,即构建一个新的对象和更新一个现有的对象。这两件事听起来有所不同,并且因为程序员可能必须要分开处理这两种情况,因此认为这两件事有所不同也是有道理的。但是从编译器的角度看,这两件事其实就是同一件事。

在应用开发环境中创建类为何如此容易,这件事的关键在本节开始的描述中已经说明。当一块内存中的字节交给了一个面向对象编程的类后,该类不必在意这块内存里存储的内容,它可以随意将其清零。有些平台能够保证这些字节从一开始就被清零。

这种对象的类永远只能构建新的实例。如果需要升级旧的实例,该怎么办?通常情况下,先找个其他地方保存旧实例的内容,通过关闭应用系统来析构旧的对象,再通过更新容纳这些对象的二进制文件来更新对象的类,启动升级后的应用系统,再通过从某种持久、简化的格式中提取旧状态的方式来创建新的对象。

2.3 数据库的类

尽管事实上,大多数的时候,数据库就是上面保存那些不被使用的对象内容的“其他地方”,在数据库开发中运用上述模式一点也不切合实际。与上述描述最接近的做法,应该是当每次想更新对象的行为时,就从旧数据库中迁移数据到新创建的更新后的对象中。对于许多数据库来说,上述做法可能仍然比许多人现在做的方式要快许多,但是因为还有另一种支持比这还要快的开发过程的做法,因此就将上述做法作为一个可选项而不再继续讨论了。

2.3.1 两条途径:创建或改变

在许多系统中,创建某“类”数据库实例有两条途径。一条途径是针对从无到有地被创建的数据库,这种情况经常发生在测试和开发的环境中;另一条途径是针对反复更新的数据库,这些更新是由于随着时间的推移,数据库的设计以行为增量的形式不断地演进而产生的,这种情况往往发生在生产环境中。

应用开发团队的工作往往是上述情况的最好的说明。一个我曾参与工作的团队拥有一个代表“数据库”的脚本,该脚本通过一些如电子邮件或网络共享文件夹这样的机制被共享,当一个开发人员搞乱了他的数据库,他会把整个数据库删除,然后运行这段脚本。当人们要设计一些有意义的变化时,他们会把这些变化写入“脚本”中。有时候一个偷懒的开发人员可能通过一些GUI,用他操作的一个数据库实例为蓝本,重新生成了这个“脚本”。

上述工作方式在开发团队中是很正常的。然而,这些开发团队编写并据此进行测试的上述脚本,永远不会在生产环境下使用。取而代之的是,他们会把新的数据库设计提交给数据库专家,数据库专家再创建一个新的数据库实例,并运行一个diff工具,来搞清楚新的数据库设计与原来相比有什么变更。当然,上述工具的输出结果不会被全盘接受,而仅仅是用来指导数据库专家编写一个新的脚本,从而实现上面的数据库设计的变更。数据库专家会手工备份生产数据库实现变更,并验证一切能够正常工作。

在我与上面这个团队一同工作期间,使用上述工作方式进行了6次产品发布,只有 1次该方式实现了完美地工作。系统所有的用户回家度周末这个后备方案,能够保护我们免受真正的灾难,帮助我们抵御重大的系统服务中断。但是,在周五夜里熬到9点半或更晚才能发布产品真的让人抓狂。

问题的根源是,开发人员试图像对待典型的面向对象编程的类一样,即仅仅通过被创建和析构去对待数据库的类。他们不操心数据库被修改了的情况,因为那是其他人的工作。

然而,全世界每一个重要的数据库都是被创建一次,然后被修改多次的。

2.3.2 难点:统一两条途径

正是由于数据库世界中这种表里不一的现象,使得定义数据库的类变成了一项困难的工作。如何才能为某件事构建一个类,使得在一种情况下,该类被从头开始创建;而在另一种情况下,该类可能在大量迭代周期里被多次构建,且这些迭代周期之间又会间隔很长的时间?

上述“两种途径”的问题并不能真正得到解决。唯一合理的解决方案是完全消除这两条途径,这意味着程序员必须找到实例化数据库的类的单条途径,使得该途径既能产生新的数据库实例,又能更新数据库的类的现有实例。

2.3.3 真实的数据库的生长情况

找到上述途径对读者来说可能是具有挑战性的,不然立刻就能看到解决方案。当难以找到一种解决方案时,看看数据库世界真正发生了什么是会有所帮助的。在这种情况下,研究一下真正的生产数据库是如何构建和成长的对解决问题会很有帮助。

在所有诸如创建空的数据库实例这样乏味的事情结束后,第一件事情就是执行一系列DDL语句,将所有数据库初始化时应具备的行为注入到数据库中。通常情况下,上述事情可以通过执行与开发团队使用的完全相同的SQL脚本来完成。

最后,当作出要以某种方式更改数据库的设计的决定后,开发人员会完成一些工作来确认更改,构建相关的功能和基础架构,并确保一切都能够组合在一起工作,然后生产数据库会被实施一个变更,使数据库的设计发生了由旧到新的改变。

过了一段时间,人们会重复上述过程来实施另一种转变,使数据库演进到下一个版本,如此这般循环往复。

一个清晰的模式正在形成:生产数据库设计的变更通常从每一个发布版本过渡到下一个版本,然后再到下一个,以此类推。这时,一个明显的问题会映入脑海:我们有可能克服变更这个问题吗?

答案是“不能”的。几乎可以这样认为,对于一个长期运转的数据库,其设计必将定期地发生变更。

2.3.4 将每个数据库构建成生产数据库会怎么样

所以,如果不能改变构建生产数据库的方式,可以考虑下面的替代方案:用构建生产数据库的方式来构建每个数据库。这样做会有什么问题吗?肯定有人会得出自己的答案,但是对于我们大多数人,对上述问题的回答是:“用构建生产数据库完全相同的方式来构建每个数据库真是非常有意义。”

规则很简单,具体如下。

1.将一个空的初始化好的数据库实例当做版本零。

2.为了获得新的数据库,构建脚本,使得数据库的版本从零过渡到1。

3.为了升级数据库,构建脚本,使得数据库的版本从N过渡到N+1。

4.当构建数据库时,从数据库的当前版本直到期望的目标版本,依次运行所有相应的脚本。

因此,要构建一个新的版本为3的数据库,可以执行版本1的脚本,接着执行版本2的脚本,再接着执行版本3的脚本。为了将数据库从版本1升级到版本3,可以执行版本2的脚本,接着执行版本3的脚本。

2.3.5 所有数据库都遵循完全相同的途径

问题的关键是,只要依次执行两个版本之间的所有脚本,就能从任意版本过渡到下一版本。要想把数据库从版本5过渡到版本7,除了按上述方式执行脚本,就不需要做额外的工作了。因为你已经知道如何从版本5过渡到版本6,并且知道如何从版本6过渡到版本7,所以你就能知道如何从版本5过渡到版本7。

只要按上述方法来做,就向测试驱动数据库开发的正确方向跨出了一大步。

2.4 增量构建

那么,该如何与上述机制进行交互呢?最好的方式应该是把数据库的每一次变更当做一个单独的版本用文档记录下来,并找到一个好方式(如版本号)来将这些变更进行排序。只要数据库能够标识已经发生了哪些变更,就能构建一个通用的机制来按正确的顺序实施正确的变更。

2.4.1 用文档记录每一次数据库的变更

开始的时候,只编写涉及一个变更的脚本。只要该脚本没有在重要的数据库上运行,就可以根据开发人员的意图,或者根据需求的变化,来随意地修改这个脚本。此时,该脚本其实是在不久的将来要做的事情的计划。

然而,当将变更提交(commit)到数据库之后,就再也不要修改上述脚本。这是变更被提交到生产环境的过渡,意味着该脚本不再是将来要发生的变更的蓝图,取而代之的是,该脚本变成了记录过去发生的事情的文档。所以从本质上讲,这个文档现在变成了历史记录。

当变更已经被提交并变成历史记录后,就可以为下一次变更创建一个新的文档。开发人员可以持续地修改这份文档,直到该文档以一种不可逆转的方式被提交到数据库为止,然后停止修改该文档,并创建另一份文档。

通过文档记录每一步骤执行的顺序也很重要。这既可以像为每份文档分配一个版本号这样的简单(如版本1是用于创建一个初始化的数据库,版本2是用于添加一些结构,等等),也可以像指出这一个过渡要先于另一个过渡这样的复杂。另外,也可能记录与上述情况完全不同的内容,比如像给一个已经提交的版本添加日期戳,来标识本次变更首次提交到生产环境的确切时间。

2.4.2 标识当前版本

数据库构建机制必须能够标识哪些变更已经施加到某个给定的数据库实例之上。这一点很重要,因为这样就可以避免将过去已经实施的变更再次施加到数据库上。要做到这一点其实比较容易,通常在数据库中创建一个表来标识在什么时间对数据库施加了哪个版本的脚本。

2.4.3 根据需要依次实施变更

在上述条件都具备后,就能构建一个机制,用来在正确的时间实施正确的变更。在只存在版本的线性增长这种最简单的情况下,即每个版本仅有一个前驱(predecessor)版本和最多一个后继版本,开发人员可以用非常低廉的成本来编写该机制。在以下章节内容中将介绍一些能够展示该机制如何工作的伪代码。在本书的配套代码中有一个实现该机制的代码示例,将其移植到你的系统平台上应该不会太困难。请访问http://maxthe3rd.com/test-driven-database-development/code.aspx来下载代码。

2.5 实现

至此,前面描述的方式能够让读者初步地刻画和实例化数据库的类。下面将介绍一些实现一个数据库的类的方法。本书先从总体需求入手,然后提供一个数据库类的伪代码实现,可以将其移植到任何平台上。

2.5.1 需求

让我们来看看一个良好的数据库的类的实例化机制应该是什么样子的。了解这些需求能够让你编写自己的机制,或者当你面对的需求明显不同于本书描述的情况时,可以调整本书建议的机制,从而适应你的具体情况。

我已经确定了4个需求,并用4个以字母C开头的单词来分别描述之,称其为“4C机制”,希望这个词儿能流行起来。

4C 机制如下所述,数据库的类的实例化机制必须是完整的(Complete),对于一个特定的版本,该机制执行了所有必要的变更;该机制必须是正确的(Correct),同样的变更从不在数据库上施加两遍;该机制必须是一致的(Consistent),每一次变更的实施,都是以完全相同的顺序来执行;最后,该机制必须是可控的(Controllable),只要想构建一个特定的数据库,读者就一定能够规定一个目标版本来构建之。

2.5.2 数据库实例化机制的伪代码

用可重复的方式来可靠地更新数据库的算法其实出奇地简单。经过多年的完善,至少与开始时搞出的东西相比,我总是惊讶于它是如此地简单。

UpgradeDatabase(<db>, <design>, <target version>)

<current version> = current version of <db>

<required versions> =

versions from <design> after <current version> and up through

➥<target version>

sort <required versions> in ascending order

for each <version> in <required versions>

execute transition for <version> against <db>

update current version of <db> to version number of <version>

上述伪代码的实现细节将留给读者来完成,但是本书通常用 XML 文档的形式来表达数据库的设计,并且把已经施加到数据库上的变更版本存储在一个数据库表中。这样,数据库的“当前”版本就能够通过选择该表最近插入的行来推断。

2.5.3 输入的伪代码

上述算法的输入可以是一个 XML 文件或一些其他格式的文本文件。只要数据库的每一个版本可以被确定下来,被适当地排序,并能在数据库实例进行实例化时声明其所需的步骤,任何格式的输入都是可以的。

为了说明这个问题,下面给读者一段简单的XML,包含了用于构建和升级一个小型数据库的脚本:

<database>

<version id="1">CREATE TABLE FOO(A I NT)</version>

<version id="3">CREATE TABLE BAR(C CHAR(30))</version>

<version id="2">ALTER TABLE FOO ADD B NVARCHAR(20)</version>

</database>

如上所示,XML文件包含了可用来排序、标识和执行SQL语句的所有信息,这些信息用来构建数据库或将数据库更新到任何特定的版本。

2.6 小结

测试执行时检查特定对象的行为。存在可靠的实例化过程很有用处,因为它确保了一个特定类的所有对象都是相同的。这种关联意味着如果测试了一个类的一个对象,就测试了这个类的所有对象,因此,看起来好像测试了这个类本身。

这就是数据库开发真正不同于传统的面向对象开发的第一点。对于存储在内存中的对象,其类的构造方法可以用销毁先前为该对象分配、在内存中的任何内容的方式,并遵循一个单一的、简单的途径来构建一个工作对象。

然而,当构建一个数据库时,照顾到那些已经保存在数据库内的数据往往是很有必要的。因此,生成数据库可以遵循两条途径:一条是升级数据库,另一条是构建一个新的数据库。这就是如今许多开发环境工作的方式,但这让我们难以获得真实的数据库的类,因此,我们也难以进行数据库的测试驱动开发。

解决的办法是迫使每一个特定的类的数据库遵循完全相同的构建途径,从而消除构建新数据库和更新现有数据库之间的区别,即定义从一个版本过渡到下一个版本所需的步骤,将空数据库当做过渡的第一步,并执行从一个版本过渡到另一个版本所必需的步骤。

按照上述方式构建数据库后,用相似的方式来表达数据库的设计:一系列增量的累积产生了数据库的最新版本。当用上述方式定义数据库后,开发人员就能轻易地确保每一次数据库版本过渡的脚本以正确的顺序被执行且仅被执行一次。

针对如何定义数据库的设计,做出上述那一点变化,并编写少量的自动化脚本;或者首先使用本书的配套代码,就可以奠定一个基础和可靠的数据库的类,据此,开发人员就可以建立测试驱动数据库开发。

本书附带的代码可以在如下网站上找到:http://maxthe3rd.com/test-driven-database- development/code.aspx。

相关图书

现代软件测试技术之美
现代软件测试技术之美
渗透测试技术
渗透测试技术
JUnit实战(第3版)
JUnit实战(第3版)
深入理解软件性能——一种动态视角
深入理解软件性能——一种动态视角
云原生测试实战
云原生测试实战
Android自动化测试实战:Python+Appium +unittest
Android自动化测试实战:Python+Appium +unittest

相关文章

相关课程