09年刚入行做程序员时就开始用Cucumber写集成测试,这也算是一种缘分吧,所以写点东西做个小结。

Cucumber 和 Rails 集成

  1. 建立一个rails项目, rails new cucumber-in-action
  2. 在Gemfile里加入下面的代码,
gem 'cucumber-rails', require: false
gem 'database_cleaner '
  1. bundle install
  2. bundle –binstubs
  3. bin/rails generate cucumber:install

现在我们就能使用Cucumber了,同时项目下面会增加几个文件,

  config/cucumber.yml
  script/cucumber
  features/step_definitions
  features/support
  features/support/env.rb
  lib/tasks/cucumber.rake

Gherkin 语法

我们可以把Cucumber当作一种系统,那么这个系统使用的语言就是gherkin,我们从一个简单的例子开始学习gherkin,


  Feature: Search courses
  Courses should be searchable by topic
  Search results should provide the course code

  Scenario: Search by topic
    Given there are 240 courses which do not have the topic "biology"
    And there are 2 courses "A001", "B205" that each have "biology" as one of the topics
    When I search for "biology"
    Then I should see the following courses:
      | Course code |
      | A001        |
      | B205        |

  • Feature 关键词定义功能,功能由名字,描述,以及一系列场景组成
  • Scenario 关键词定义场景, 场景由名字,以及一系列步骤组成,场景类似于其他测试框架中的测试用例
  • Given, When, Then, But, And 这些关键词定义步骤

对于我们人类来说 Given, When, Then, But, And 这5个关键词是有各自语义的,但是在定义步骤时,Cucumber对 它们是不加区分的,比如说,

  Given no zuo no die
  When no zuo no die
  Then no zuo no die
  But no zuo no die
  And no zuo no die

这5个步骤其实是一样的,但是为了人类特别是你的产品经理或者合作伙伴能看懂你写的场景,最好是区分使用这5个关键词。

除了例子中提到的几个关键词之外,还有

  • Background 关键词定义背景,类似于其他测试框架中的before, setup等
  • Scenario Outline 关键词定义场景大纲,和例子(Examples)或者场景(Scenarios)搭配使用,用于构造同类的场景的组,此时的例子和场景在英文中必须是复数
  • Examples 关键词定义例子,和场景大纲搭配使用

Scenario OutlienExamples的搭配使用稍微有点复杂,我会在后面介绍它们的用法。

让 gherkin 代码跑起来

我们看到使用 gherkin 写的”程序”直白易懂,因为它几乎就是我们平时用的人类语言,只不过更加规范更加书面。 为了让我们的 gherkin 代码跑起来,我们把上面的例子保存到 search_course.feature 文件中,这个过程是不是很熟悉,就像我们把 ruby 代码保存到某个以.rb 结尾的文件里。

执行 cucumber features/search_course.feature,此时Cucumber会把那些还未实现的步骤(steps)打印出来,这真是一个贴心的功能。

You can implement step definitions for undefined steps with these snippets:

Given(/^there are (\d+) courses which do not have the topic "(.*?)"$/) do |arg1, arg2|
  pending # express the regexp above with the code you wish you had
end

Given(/^there are (\d+) courses "(.*?)", "(.*?)" that each have "(.*?)" as one of the topics$/) do |arg1, arg2, arg3, arg4|
  pending # express the regexp above with the code you wish you had
end

When(/^I search for "(.*?)"$/) do |arg1|
  pending # express the regexp above with the code you wish you had
end

Then(/^I should see the following courses:$/) do |table|
  # table is a Cucumber::Ast::Table
  pending # express the regexp above with the code you wish you had
end

我们在features/step_definitions目录里创建一个叫 search_cource_steps.rb 的文件,接着将cucumber打印出来的steps保存到此文件里, 然后做一些小的修改我们就有一了份可运行的steps了,


Given(/^there are (\d+) courses which do not have the topic "(.*?)"$/) do |num, topic|
  pending # express the regexp above with the code you wish you had
end

Given(/^there are (\d+) courses "(.*?)", "(.*?)" that each have "(.*?)" as one of the topics$/) do |num, code1, code2, topic|
  pending # express the regexp above with the code you wish you had
end

When(/^I search for "(.*?)"$/) do |topic|
  pending # express the regexp above with the code you wish you had
end

Then(/^I should see the following courses:$/) do |codes_table|
  # table is a Cucumber::Ast::Table
  pending # express the regexp above with the code you wish you had
end

Gherkin 本地化

可以使用命令 cucumber –i18n help 查看支持的语言,很幸福,Cucumber 支持我们大简体中文。 通过 cucumber –i18n zh-CN 查看 Gherkin 对应的中文关键词,

| feature          | "功能"                       |
| background       | "背景"                       |
| scenario         | "场景", "剧本"                |
| scenario_outline | "场景大纲", "剧本大纲"          |
| examples         | "例子"                       |
| given            | "* ", "假如", "假设", "假定"   |
| when             | "* ", "当"                   |
| then             | "* ", "那么"                 |
| and              | "* ", "而且", "并且", "同时"   |
| but              | "* ", "但是"                 |
| given (code)     | "假如", "假设", "假定"         |
| when (code)      | "当"                         |
| then (code)      | "那么"                       |
| and (code)       | "而且", "并且", "同时"         |
| but (code)       | "但是"                       |

我们这些英文不好的土鳖也可以用中文写 cucumber feature 了。

彩票追号

彩票追号是我曾经开发过的并且现在正在维护的一个项目,在做这个项目之前,我根本就不知道彩票追号是怎么回事。 开发前和项目经理以及相关的运营人员开了一些会做了一些讨论,大概知道了追号的作用,简单来说就是帮助客户用同一个号码投连续注多期彩票, 核心功能就是这么简单,一句话就能说清楚,但是当后面涉及到很多的业务细节时就开始变的复杂起来,比如 追号时怎么选择渠道(这个一般由运营人员通过设定渠道的优先级别和彩种的开关来控制),要求在规定的时间内投注成功尽可能多的订单 (一些快频彩,比如重庆时时彩的彩期时间只由十分钟,这些彩种对此要求比较严格),还有各种异常处理等等。

我整理下了思路,提炼出项目经理认为的最紧要功能:

  1. 新期到的时候,帮助客户自动投注彩票号码
  2. 运营人员通过设定渠道优先级等参数,控制投注渠道
  3. 投注失败时能够重试(自动或者手动)
  4. 能够在规定时间内投注大批量的快频彩订单

由于涉及到金钱并且订单量不小(估计每天会有至少3万多笔的追号订单),所以我决定不要立即动手写代码,而是前期多花些时间把业务需求想明白些,于是我决定使用 Cucumber 来写 feature。拿追号这个 feature 来举例,彩票分好多种,有双色球,排列三,大乐透,重庆时时彩,福彩3D等等,每个彩种又分好多玩法,我决定从双色球入手,经过多次修改,我写了 双色球的追号 feature,


# language: zh-CN

功能:追双色球
  为了方便客户使用相同的号码连续投注多期双色球

  背景:
    假设系统里有下面的彩种:
    |    name        | lotno  | money |moneymulti  | betmore |
    |    双色球       | F47104 | 2     | 1          | 0       |

  场景大纲: 追双色球
    当客户投注"<彩种>"
    并且客户选择号码"<注码>"进行投注
    并且客户选择"<倍数>"倍
    同时客户追"<期数>"期
    当第"1"次新期入库后
    同时系统检测到新期
    并且投注"成功"
    那么客户应该增加"1"条新的投注记录
    并且投注号码是"<注码>"
    同时客户的投注结果应该成功
    同时客户投注结果应该入库成功
    当第"2"次新期入库后
    同时系统检测到新期
    并且投注"成功"
    那么客户应该增加"1"条新的投注记录
    并且投注号码是"<注码>"
    同时客户的投注结果应该成功
    同时客户投注结果应该入库成功
    当第"3"次新期入库后
    同时系统检测到新期
    并且投注"成功"
    那么客户应该增加"1"条新的投注记录
    并且投注号码是"<注码>"
    并且客户的投注结果应该成功
    同时客户投注结果应该入库成功
    同时客户的追号应该结束
    当第"4"次新期入库后
    那么客户不会产生新的投注记录

  @hd-ld
  例子: 红单蓝单
    | 彩种   |   注码                  | 倍数 | 期数 |
    | 双色球  | 010203040512~01        | 1   | 3    |
    | 双色球  | 010203040510~01        | 5   | 3    |
    | 双色球  | 010203040513~01        | 16  | 3    |

  @hf-ld
  例子: 红复蓝单
    | 彩种   |   注码                  | 倍数 | 期数 |
    | 双色球  | 010203040506070809~01  | 1   | 3    |
    | 双色球  | 010203040508091013~01  | 11  | 3    |
    | 双色球  | 010203040508091516~01  | 6   | 3    |

  @hd-lf
  例子: 红单蓝复
    | 彩种   |   注码                  | 倍数 | 期数 |
    | 双色球  | 060712161833~0710      | 1   | 3    |

  @hf-lf
  例子: 红复蓝复
    | 彩种   |   注码                  | 倍数 | 期数 |
    | 双色球  | 0102030405060708~0102  | 1   | 3    |
    | 双色球  | 0102030405080910~0103  | 10  | 3    |
    | 双色球  | 0102030405080915~0204  | 7   | 3    |

  @hdt-ld
  例子: 红胆拖蓝单
    | 彩种   |   注码                  | 倍数 | 期数 |
    | 双色球  | 01*02030405060708~01   | 1   | 3    |
    | 双色球  | 0102*030405080910~03   | 23  | 3    |
    | 双色球  | 010203*0405080915~04   | 4   | 3    |

  @hdt-lf
  例子: 红胆拖蓝复
    | 彩种   |   注码                  | 倍数 | 期数 |
    | 双色球  | 01*02030405060709~0102 | 1   | 3    |
    | 双色球  | 010203*0405080912~0304 | 32  | 3    |
    | 双色球  | 01020304*05080916~0405 | 2   | 3    |

  @hh
  例子: 混合
    | 彩种   |   注码                           | 倍数 | 期数 |
    | 双色球  | 32*031221223031~04              | 1   | 3    |
    | 双色球  | 030817192326~04                 | 1   | 3    |
    | 双色球  | 16*0506152526~16                | 1   | 3    |
    | 双色球  | 061516172627~06                 | 3   | 3    |
    | 双色球  | 061718*26272829~07              | 1   | 3    |
    | 双色球  | 01112122313233~01*              | 1   | 3    |
    | 双色球  | 01111221223132~02*              | 1   | 3    |
    | 双色球  | 212232*01111231~02*             | 1   | 3    |
    | 双色球  | 101120252728~12                 | 1   | 3    |
    | 双色球  | 060712161833~0710               | 1   | 3    |
    | 双色球  | 0608101213151619202532~060709   | 1   | 3    |
    | 双色球  | 010902040610132328~15^          | 1   | 3    |

用母语写 feature 很爽,更因为我需要把这个feature给项目经理以及其他相关的同事看,所以用中文写是最好的选择。

# language: zh-CN, 是告诉 Cucubmer 这个 feature 是用中文写的,需要用中文去解释。

背景 定义了步骤 假设系统里有下面的彩种:, 通过这个步骤我们在跑每个场景或者例子之前都可以创建好们需要的数据或者其他东西,在这里是创建了双色球这个彩种。

此步骤的实现如下所示:


假设 /^系统里有下面的彩种:$/ do |lot_type_table|
  ...
end

Tables

lot_type_table 是一个Cucumber::Ast::Table对象,这种对象可以帮助我们方便地创建数据。

比如:

|    name        | lotno  | money |moneymulti  | betmore |
|    双色球       | F47104 | 2     | 1          | 0       |

通过 lot_type_table.hashes 我们可以得到一个 hash 数组:

[{"name"=>"双色球", "lotno"=>"F47104", "money"=>"2", "moneymulti"=>"1", "betmore"=>"0"}]

更详细的内容,可以参考: http://cukes.info/api/cucumber/ruby/yardoc/Cucumber/Ast/Table.html

场景大纲和例子

在平时的编程过程中,我们会提醒自己注意DRY(Don’t Repeat Yourself),也就是不要拷贝代码,尽量减少重复代码,现在我们用 Cucumber 写 feature,其实也是在编程,用的编程语言就是 gherkin, 场景大纲是我们实施DRY的一种重要手段,使用场景大纲不但使我们的 feature 简洁易读,同时减少了代码量,易于维护。

我们将红单蓝单这个例子展开,就是三个场景,我们分析下其中的一个场景,


功能:追双色球
  为了方便客户使用相同的号码连续投注多期双色球

  背景:
    假设系统里有下面的彩种:
    |    name        | lotno  | money |moneymulti  | betmore |
    |    双色球       | F47104 | 2     | 1          | 0       |

  场景: 追双色球
    当客户投注"双色球"
    并且客户选择号码"010203040512~01"进行投注
    并且客户选择"1"倍
    同时客户追"3"期
    当第"1"次新期入库后
    同时系统检测到新期
    并且投注"成功"
    那么客户应该增加"1"条新的投注记录
    并且投注号码是"010203040512~01"
    同时客户的投注结果应该成功
    同时客户投注结果应该入库成功
    当第"2"次新期入库后
    同时系统检测到新期
    并且投注"成功"
    那么客户应该增加"1"条新的投注记录
    并且投注号码是"010203040512~01"
    同时客户的投注结果应该成功
    同时客户投注结果应该入库成功
    当第"3"次新期入库后
    同时系统检测到新期
    并且投注"成功"
    那么客户应该增加"1"条新的投注记录
    并且投注号码是"010203040512~01"
    并且客户的投注结果应该成功
    同时客户投注结果应该入库成功
    同时客户的追号应该结束
    当第"4"次新期入库后
    那么客户不会产生新的投注记录

我们看到例子中的”彩种”, “注码”,”倍数”, “期数”的值被分别代入到场景大纲中的”<彩种>", "<注码>", "<倍数>","<期数>"中,即形成一个场景。这个功能非常有利于我们构造大量的场景,从而提高我们系统的健壮性。

标签

功能:追双色球里有许多 @hd-ld, @hf-ld之类到符合,这些符合就是标签,我们可以使用标签运行某个特定的例子或者场景, 比如下面的命令就只会运行__红单蓝单__这个例子,

  bin/cucumber features/ssq.feature -t @hd-ld

标签更多的用法可以通过 bin/cucumber –help 了解。

World

如果我们想使用 Rspec 的 Matcher,该怎么做呢? 这时我们可以通过 World 方法将 Rspec 的 Matcher 融入到 Cucumber(记得将rspec加到Gemfile中)。 首先在features/support 目录下创建一个叫 expections.rb 的文件,加入代码,

require 'rspec/expectations'
World(RSpec::Matchers)

然后我们就可以在 step 里使用 Rspec 的 should 等方法了。

假设 /^系统里有下面的彩种:$/ do |lot_type_table|
  1.should == 1
end

其实我们可以通过world将任何我们想要的方法加入到 step 中,

  module Foo
    def foo
	end
  end

  World(Foo)
假设 /^系统里有下面的彩种:$/ do |lot_type_table|
  foo
end

Fabrication

做测试时少不了构造各种数据,我推荐使用Fabrication,主要原因是配置简单,使用方便,文档齐全。详细的文档可以访问http://www.fabricationgem.org/

将 fabrication 加到 gemfile,

  gem fabrication

Fabrication 能够自动加载所有你定义好的 Fabricators,

spec/fabricators/**/*fabricator.rb
test/fabricators/**/*fabricator.rb

我们创建一个 user_fabricator.rb 文件将其放到 spec/fabricators/ 目录下,user_fabricator.rb 的内容如下,

 Fabricator :user do
   name 'jim'
   email 'jim@mail.com'
 end

创建一个 user,

  Fabricate :user

或者创建时修改默认属性,

  Fabricate :user, name: 'jim01'