1

我们一般将测试放在项目的最后时刻进行,甚至在时间较紧时、预算超支,或者其他原因发生时会放弃测试。

项目的管理者好奇为什么开发者就是不能一开始就明白(需求、设计),而在系统有很多利益相关者并且不同的相关者对系统有不同的看法的时候,开发者(特别是在大型项目中),更容易变得迷糊,使得协商过程像盲人摸象一样。

每个项目的开始,必然是有一个关于项目行为表现、功能特点的讨论会,由客户或者其他业务人员向开发团队解释他们就行想要什么。(苦逼又令人讨厌的策划....)

有时候,这些交互、讨论以敏捷开发形式表现;有时是设计文档,就像去年查理斯-福克斯的博客所说的那样;有时是由Keynote制作的流程图或者模型;有时甚至是一个简单的电话解释而已。

仅通过这些沟通,开发者一般只是负责构建一个能够运行的系统而已,而这对于一个开发团队来说,是远远不够的。这(单纯的沟通)对于大型系统的业余开发人员来说尤其困难。

为什么不进行测试?

一般存在一个争议:如果客户/业务人员一开始就对系统的行为、特征有充分的认知,那么为什么往往要撤销对这些功能、行为进行测试?

答案可能非常简单:测试一般被认为是共享资产(对大家都有用的),也不被认为是对项目开发有实际价值的。测试只对工程师有用或者只对特定的一些人有用。

那么如何才能使得测试对大家都有价值呢?仅仅是列出系统的功能特性吗?当然不是,我们应该使用behavior-driven development (BDD)而不是仅仅是test-driven development (TDD)。

BDD是什么?

行为驱动开发应该着眼于你的代码所要实现的业务行为,即“为什么要编写这样的代码?”它可以很好的支撑项目核心工作流程,特别是对于交叉功能的了解与实现。

敏捷BDD开发有很大的好处。当开发者和敏捷项目主或者业务分析师坐在一起,将大概功能框框(具体如何实现由开发者在框内填写)写在白板上:

  1. 业务人员指定系统的行为特性

  2. 开发者基于他们自己对系统的理解向业务人员提问,同时从开发者角度写下其他附加的行为。

理想情况下:项目的参与人员能根据当前系统行为列表判断新加入的功能行为是否会破坏现有功能。
图片描述
我发现这些简单的行为给了我一些约束,使得我能像一个开发者一样思考:这些我已经实现的这些测试能够将我的实现代码约束在一个规范之中。而那些功能代码只需满足这些约束、规范,就能在协作开发中快速完成。

这种协作方法使得我更加专注于提供给最终用户的功能特性,而且业务人员可以在旁约束、纠正我对系统行为的理解,而不是系统的具体实现。这就是BDD和TDD的突出区别。

BDD的一个例子

情景:你是负责开发企业会计系统的团队一员,系统使用Rails框架实现。有一天,业务人员问你一个关于提醒模块的功能:提醒用户他们正在等待处理的发票。你坐下来和业务人员定义这个功能模块。

你打开你的文本编辑器/笔记本,开始在上面画上框框,每个框代表用户需要的功能行为:

//为每个新支票添加一个提醒日期
it "adds a reminder date when an invoice is created"
//当提醒日期到来就发邮件提醒
it "sends an email to the invoice's account's primary contact after the reminder date has passed"
//如果用户阅读了邮件就给用户打上标记
it "marks that the user has read the email in the invoice"

在开发中专注于系统行为使得测试在验证你所实现系统行为是否正确中是十分有用的,而不仅仅是编码正确(没有bug)。要注意的是,这种分析要用业务语言而不是实现系统所采用的具体开发语言。
你不需要将“发票属于哪个用户”描述出来,因为开发团队之外的人也并不关心这种关系。

有些开发者在讨论/开发现场就写出测试样例,在系统中调用这些所要测试的方法,设置期望值,如下:

it "adds a reminder date when an invoice is created"  do
  current_invoice = create :invoice
  current_invoice.reminder_date.should == 20.days.from_now
end

这些测试样例必然是运行失败的,因为我们还没有实现设置remind_date的代码。

失败的测试

我明白开发者为什么会写失败样例测试,但是从业务人员的角度来说,这写测试对他并无用处。一些业务人员可能会被这些测试细节、实现细节搞迷糊,甚至我学得一些开发知识后就插手开发人员的工作。(数据库设计、代码复用)

从我的经验来开,如果开发者对于特定系统行为写出多行实现概要,业务人员会感到不耐烦,他们会觉得这是浪费他们的时间并不耐烦的急于阐释他们假想的下一个系统行为。

BDD和TDD的区别

现在我们从另一个角度看:使用TDD方法,并且写出测试概要:

//创建支票后,过期日期=创建日期+20天
it "after_create an Invoice sets a reminder date to be creation + 20 business days"
//Account#primary_payment_contact返回支付联系人或者用户项目管理者
it "Account#primary_payment_contact returns the current payment contact or the client project manager"
//InvoiceChecker#mailer 检查是否过期,如果是,就发邮件提醒。
it "InvoiceChecker#mailer finds invoices that are overdue and sends the email"

这些测试是有用的,不过只是对一些人有用:工程师。BDD是用来沟通(交叉功能)项目成员的工具,包括开发者和业务人员。

通过暂时挂起(不实现)具体行为,你可以进行测试优先的开发。首先,编写测试;接着,运行测试(当然是运行失败的,因为我们都还没开始实现具体行为);编写行为,使他能跑;修正,使他能正确运行。

BDD社区的很多工作和产品都使得测试中的断言检查读起来向普通语言一样。下面是一个刻板老套的RSpec测试:

a = 42
a.should == 42

这个格式使得结果易于阅读和理解。但注意这不是我们在此所应该做的,我们应该尽快获取系统行为的准确描述,并且坚持“每个系统行为都要测试”的原则。而从之前白板上画框框的工作,我们基本能够知道系统行为是什么。

BDD不是修正编码的奇特方式,它只是用来让团队成员(包含业务人员、顾客)对系统行为进行沟通而已。

BDD是关于协作和沟通的

再回看刚才的例子:企业会计系统。

你和业务人员讨论项目的功能:你(开发者)从内部(各模块是如何协作的)分析系统,而他们(业务人员)就从外部分析。

你会思考一些情况并且并对系统分析师(业务人员)就一下情况进行提问:

//默认的提醒日期是?在支票到期前的第几天提醒?
* What's the default reminder date going to be? How many days before the invoice due date?
//这些天数是指自然日还是工作日?
* Are those business days or just calendar days?
//如果这些支票所属的账号主没有对应的联系方式,那该怎么办?
* What happens if there's not a primary contact associated with the account?

因此,使得业务人员理解你的问题是非常重要的,因为他们可能对具体开发缺乏相应的知识。

有时候,BDD是一种有益于两个部门(如策划和开发)协作和沟通的工具,也是清晰划分系统功能界限和对开发团队(如预计开发时间)有更好估计的一种方法。
可能你意识到无法从给定日期计算10天以后的日期(因为每个月的天数都不一样),那么你就需要实现这个计数功能,而业务人员对这个计数功能可能并不关心。

开发者有对于具体开发的思考(比如你说的‘天’是什么),业务人员也有他们的思考(如:请不要使用’过期’这个词语了,有时候它有不同的意思)。因此,只由一方来考虑系统功能和测试就会抹杀掉另一方的有价值的观点。

当然如果业务人员或者客户不能和开发者共处一室的时候,让他们将期望的系统行为和开发者自己的分析、理解写在纸上也是一个有效的沟通方法。


曾纪文
201 声望22 粉丝