行为驱动开发之二,实施篇

  推行并实施行为驱动开发(Behavior Driven Development, BDD)已有三周。(行为驱动开发,即在编写产品代码前,先将产品功能描述成功能点(Feature),再对其间的步骤进行实现。在代码完成后,用事先写好的Feature对其功能进行验证。我们使用的Feature描述工具是Cucumber,对Cucumber不了解的人,可以先跳到最后,我写了一个简单的例子,说明Cucumber的工作方式。行为驱动开发的好处,可以参考前文)

第一周。

  整天拿着笔记本满办公室跑,走到开发人员的桌子边。 问:你实现了什么功能? 然后以Cucumber的格式把他/她的描述写成场景(Scenario)。 又问:我写下来,你看看,是这样么? 跟开发人员商定好了功能后,进一步规范文法,将类似的步骤统一成一种表示法,并以数据驱动方式编写所需的输入输出数据。 再问:当我从这里输入这个值,从哪里拿到输出呢? 这个时候,注重可测试性,即一个功能是否容易被证实有效或失效。 上述三个问题结束,就生成了规范的功能点(Feature)的描述了。然后,回到自己座位上,为每个Scenario编写具体实现的步骤。

第二周。

  制作BDD/Cucumber的幻灯片,对小组成员进行知识普及。演示中也听到了很多新鲜的问题,如:为何Given/When/Then可以描述所有情景等。

第三周。

  组内已经有若干人员边学边进行Feature的编写,而我的大部分时间也都花在了解疑释惑,帮助新人上手,以及如何提供更好的API支持。

  这三周,是我在公司五年里第一次跟开发人员如今紧密的协同工作。一起商量一个程序应该如何工作(behave),如何与用户交互(interaction),如何安全地限制用户输入与友好地提供输出(input and output),最重要的,我们要一起决定当该程序完成后,它应该如何表示出自己正有效的工作(testability)。作为一个测试人员,可测试性与输入/输出是我最擅长的,而开发人员则擅长编码,一直提醒我要保持测试架构(Fixtures/API)的灵活与易懂。

   虽然这些经历对于传统的项目组或者早已踏入敏捷的团队,可能没有共鸣。但对于任何一个从传统项目向敏捷转换的项目组,都会从中找到似曾相识的影子,所以我从中挑选了一些比较典型的故事,分享给大家。

第一个故事:

  你根本是在用现有系统测试你的Cucumber脚本

  这是跟我一起工作了一周的开发人员H对我说的一句话,”你这不是在测试我的系统,你分明是在用我的系统测试你的自动化脚本“。 在同他一起编写功能点描述时,产品代码已然提交了,H按记忆对功能做了如下描述:把事件A插入数据库中事件表(event_table),把规则A插入数据库中的规则表(rule_table),运行匹配规则后,事件A应该被打上标签,表明它匹配了规则A。 我整理后,写成了Feature文件,可运行后,事件A并没有匹配规则A。请H现场调试,他告诉我要追加一条记录,事件A必须同时存在于两张数据库表,匹配才能进行。我修改后运行,还是没有匹配。他又再次调试,发现事件A还需要另一张数据库表的支持,才能进行匹配。于是我又一次修改,才算成功。H很郁闷的进行了上述的抱怨,”你分明是在用我的系统测试你的自动化脚本“。

  由于代码变化快,很难保持与文档的一致性。所以我组里开发人员的口头描述成了唯一的功能描述。但是代码一旦完成,开发人员也未必能够完全描述具体的功能,这就导致按照需求和功能编写的自动化代码很难与产品代码相匹配。而如果产品代码先于自动化代码产生,那么做修改的只能是自动化代码,而非产品代码,这就变成了白盒测试。即按照既有代码的既有功能进行验证,而非验证代码是否满足预期需求。

  而开发人员的这句话,正说明了,BDD中,规范(specification)与功能点(Feature)必须先于代码完成的必要性与重要性。如果上述故事里,我们先写好Feature文件,那么要修改的,就是产品代码。 在于另一个开发人员C的协同工作时,我就避免了这种情况的发生。虽然C的代码也提交了,但是一起编写Feature文件时,我说,“别想着你的代码实现了什么功能,就照着它应该实现什么功能来写”,如此一来,写成的20对输入输出的Feature,在运行时,只有10对通过,另外10对,如今的代码并不支持。虽然现在并没有把这个任务加到任务表,但我们已经有了一个基本的认识,这样的功能应该支持何种输入输出,而我们的代码又实现了哪些。

第二个故事:

  聊聊天找问题

  用户希望我们的系统可以定期清理过于老旧的数据,在清理前可以发信给用户进行确认。我们需要实现的功能如下:1可以配置是否打开提醒;2可以配置提醒后是否删除,即只提醒但不自动删除。在与开发人员T坐下来一起编写Feature时,此功能的代码已经实现了一大半。 我的第一个Feature如下:

Given 用户有一个老旧数据
When 管理员打开此功能
Then 用户收到提醒邮件,内容包括:你的旧数据不久会被删除
And 不久后,数据被删除

Given 用户有一个老旧数据
When 管理员打开此功能,但是屏蔽了自动删除
Then 用户收到提醒后见,内容包括:你有一份旧数据
And 不久后,数据未被删除

  写到这里,T一拍大腿,说糟了,我只写了一份邮件模板。不管管理员是否开启自动删除,都会告诉用户,旧数据即将被删除。听到这里,你也许会纳闷,这种问题怎么会留到此时才被发现,这同我们组的开发模式有关,我笑称我组的开发模式为迅雷模式,有空我会写一篇模式分析,来专门讨论一下这个手工作坊式的开发模式。这里我想说的是,只有当你和开发人员坐下来,具体地讨论到每一个细节,才能帮助开发人员尽早发现这些问题。

第三个故事:

  培养可测试性

  接着第二个故事向下说,我注意到此功能里涉及到了用户会接受邮件的这个功能。因为我们的测试环境在单独的网络拓朴里,而那个环境里我们并没有搭建电子邮件服务器,所以接受/发送电子邮件的功能,我们都是等到做系统测试(system testing)才进行的。因为代码尚在开发期,所以我跟T商量,说咱们怎么能不出内网就能知道你有发信呢。

   解决办法有两个,1,做一个发信的Mock,让产品调用,然后把被调用的信息发送给我的测试系统;2,把发信内容打印到调试的日志里。我们选择了2,简单直接。 从这个例子,我俩意识到,在产品代码未提交前,只要加入可测试性的思考,就可以把一个本来不太容易测试到的功能,通过一些小手段测试到。而这些小办法,小手段,有时甚至不需要增加额外开销,却可以极大的增加测试效率。

  我的故事讲完了,虽说精简了很多技术细节,但每个故事都是真实的。每一个故事里,我都提到了开发人员,因为我确实是每一个功能点,都是在与开发人员的协同工作下完成的。 在结束前,依然要提一些BDD过程中遇到的问题。我依然很惋惜的见到有人孤军奋战,一个人埋头编写Feature描述。有时,这孤军是开发人员,觉得自己可以清楚地描述即将实现的功能;有时候,这孤军是测试人员,已经跟开发人员接触过,有能力独自完成任务。

  然而,当我去走查(Review)他们完成的Feature文件,一眼就能看出,这不是一份经过深思熟虑的描述,其中包含了些含糊的步骤或过分简单的数据。 跟H聊天时,提到这个问题,他也一再告诫我:“如果BDD只对你一个人适用,那么它就不是一个好方法。”第一步,Cucumber的使用,BDD的概念,已经成功的推广了。接下来,最艰难的实施部分,如何改变组员单独作战,只重速度和数量,不重质量的习惯。以及如何让大家习惯于协同作战呢。

Cucumber例子:

  我希望实现一个文件管理系统的存档功能,在设计和编码前,我先用Cucumber将我需要的功能描述出来。

Feature:archive old record
  As a admin
  I want to archive old records
  So that I can keep my current records list small and clean

  Scenario: archive old record 
    Given 1 record "A" with date "2011-03-07" 
    And 1 record "B" with date "2011-03-17" 
    When I run "old record archive" 
    Then system should move record "A" into archive forlder 

  针对每个步骤,如Given 1 record "A" with date "2011-03-07",编写其对应的ruby代码,如:

/Given 1 record "" with date ""/ do |id, date| 
  create_record("id"=>id, "date"=>date) 
  #此处,create_record会被调用2次,第一次参数是A, 2011-03-07,第二次参数是B,2011-03-17 
end 

/When I run ""/ do |cmd| 
  system(cmd) 
  #此处,system会被调用,参数是old record archive 
end 

/Then system should move record "" into archive folder/ do |record|
  search_record_in_archive_folder(record).should == true 
  #此处,search_record_in_archive_folder会被调用,record为“A" 
end

  


posted @ 2011-03-21 09:06  jarodzz  阅读(2177)  评论(4编辑  收藏  举报