Cucumber 实例

Table of Contents

1 创建feature文件

$ cd ./cucumber-exercise/
$ cucumber
You don't have a 'features' directory.  Please create one to get started.
See http://cukes.info/ for more information.

依照提示,需要建立目录features

$ mkdir features
$ cucumber
0 scenarios
0 steps
0m0.000s

建立一个文件./features/cash\withdrawal.feature, 内容为:

Feature: Cash Withdrawal
  Scenario: Successful withdrawal from an account in credit
    Given I have deposited $100 in my account
    When I request $20  
    Then $20 should be dispensed

cucumber关键字,

  • Feature, 可从下一行开始添加对Feature的说明文字,直到遇到其他的cucumber关键字。
  • Scenario, 说明文字的添加。一个Feature可包含多个Scenario,每个Scenario运行时,都会自动创建一个World,World是Scenario中每个step的运行环境,相当于包含了该Scenario所有方法的一个ruby类。
  • step, 一个Scenario包含多个step,需要为每个step都定义step defination。
    • Given
    • When
    • Then

2 创建step definition文件

在./features目录,建立文件夹step-definations(可改)。cucumber默认会在./feature目录下搜寻匹配的step definition。

创建Ruby脚本文件./features/step-definations/steps.rb,内容:

Given /^I have deposited \$(\d+) in my account$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

When /^I request \$(\d+)$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

Then /^\$(\d+) should be dispensed$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

脚本文件中,使用正则表达式来匹配feature文件中对应关键字后面的字符串(自动过滤掉开始和结尾的空白字符),正则表达式的括弧用于提取原字符串中字符,用以作为函数定义的实参。 添加实际功能。

class Account
  def deposit(amount)
    @balance = amount
  end

  def balance
    @balance
  end
end

class Teller
  def initialize(cash_slot)
    @cash_slot = cash_slot
  end

  def withdraw_from(account, amount)
    @cash_slot.dispense(amount)
  end
end

class CashSlot
  def contents
    @contents or raise("I'm empty!")
  end

  def dispense(amount)
    @contents = amount
  end
end

Given /^I have deposited \$(\d+) in my account$/ do |amount|
  @my_account = Account.new
  @my_account.deposit(amount.to_i)
end

When /^I request \$(\d+)$/ do |amount|
  @cash_slot = CashSlot.new
  teller = Teller.new(@cash_slot)
  teller.withdraw_from(@my_account, amount.to_i)
end

Then /^\$(\d+) should be dispensed$/ do |amount|
  @cash_slot.contents.should == amount.to_i

2.1 Transform

上述代码,多次将匹配的参数由字符串转换为整型,即多次使用方法amount.to\i。cucumber方法Transform用于去除这些累赘。Transform之后的结果:

class Account
  def deposit(amount)
    @balance = amount
  end

  def balance
    @balance
  end
end

class Teller
  def initialize(cash_slot)
    @cash_slot = cash_slot
  end

  def withdraw_from(account, amount)
    @cash_slot.dispense(amount)
  end
end

class CashSlot
  def contents
    @contents or raise("I'm empty!")
  end

  def dispense(amount)
    @contents = amount
  end
end

CAPTURE_NUMBER = Transform /^\d+$/ do |str|
  str.to_i
end

Given /^I have deposited \$(#{CAPTURE_NUMBER}) in my account$/ do |amount|
  @my_account = Account.new
  @my_account.deposit(amount)
end

When /^I request \$(#{CAPTURE_NUMBER})$/ do |amount|
  @cash_slot = CashSlot.new
  teller = Teller.new(@cash_slot)
  teller.withdraw_from(@my_account, amount)
end

Then /^\$(#{CAPTURE_NUMBER}) should be dispensed$/ do |amount|
  @cash_slot.contents.should == amount
end

在为feature中的step匹配step defination过程中,会检查是否有匹配的Transform,有的话,会返回Transform执行后的结果,这里将该结果给了代码块作为形参amount。

2.2 为World添加ruby自定义方法

再一次说明,World是Scenario的运行环境,可理解为一个ruby类,其中包括了该Scenario所有方法(包括step defination)及类变量等。每个World都是因某个Scenario 而生,随着这个Scenario而亡。

为了为World添加ruby自定义方法,需要定义一个ruby module(如,KnowsTheDomain),然后,使用cucumber的方法World将该ruby module混入World。

class Account
  def deposit(amount)
    @balance = amount
  end

  def balance
    @balance
  end
end

class Teller
  def initialize(cash_slot)
    @cash_slot = cash_slot
  end

  def withdraw_from(account, amount)
    @cash_slot.dispense(amount)
  end
end

class CashSlot
  def contents
    @contents or raise("I'm empty!")
  end

  def dispense(amount)
    @contents = amount
  end
end

CAPTURE_NUMBER = Transform /^\d+$/ do |str|
  str.to_i
end

module KnowsTheDomain
  def my_account
    # Using ||= ensures that the new Account will be created only once,
    # even though method my_account is called serveral times here. 
    # puts "cash_slot"
    # puts "my_account"
    @my_account ||= Account.new
  end

  def teller
    # puts "teller"
    @teller ||= Teller.new(cash_slot) #call module's method "cash_slot"
  end

  def cash_slot
    # puts "cash_slot"
    @cash_slot ||= CashSlot.new
  end
end

World(KnowsTheDomain)

Given /^I have deposited \$(#{CAPTURE_NUMBER}) in my account$/ do |amount|
  # puts self #discover all modules mixed into this "World"
  my_account.deposit(amount)
end

When /^I request \$(#{CAPTURE_NUMBER})$/ do |amount|
  teller.withdraw_from(my_account, amount)
end

Then /^\$(#{CAPTURE_NUMBER}) should be dispensed$/ do |amount|
  cash_slot.contents.should == amount
end

2.3 组织代码

2.3.1 剥离ruby代码

将三个类剪切到新创建的./features/../nice\bank.rb中,然后,

  • 在./features/steps.rb中添加:
require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'nice_bank')
  • ./features/support

当cucumber开始运行一个feature,会在加载step defination之前,把./features/support目录下的所有ruby文件都加载(注意,这些ruby文件之间不应该有依赖关系)。有一点需注意,./features/support/env.rb总是第一个被加载进去。 这里,可以将

require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'nice_bank')

放入./features/support/env.rb。

2.3.2 剥离Transform

将以下代码从./features/steps.rb中剪切到新建的./features/support/transform.rb中。

CAPTURE_NUMBER = Transform /^\d+$/ do |str|
  str.to_i
end

2.3.3 剥离World Module

将以下代码从./features/steps.rb中剪切到新建的./features/support/worldextentions.rb中。

module KnowsTheDomain
  def my_account
    # Using ||= ensures that the new Account will be created only once,
    # even though method my_account is called serveral times here. 
    # puts "cash_slot"
    # puts "my_account"
    @my_account ||= Account.new
  end

  def teller
    # puts "teller"
    @teller ||= Teller.new(cash_slot) #call module's method "cash_slot"
  end

  def cash_slot
    # puts "cash_slot"
    @cash_slot ||= CashSlot.new
  end
end

World(KnowsTheDomain)

2.3.4 分离step defination

最好是将众多的step defination以实体(domain entity)为单位分别放到不同的文件中。这里,将用以下3个文件替代./features/steps.rb。

  • ./features/step\definitions/account\steps.rb
Given /^I have deposited \$(#{CAPTURE_NUMBER}) in my account$/ do |amount|
  # puts self #discover all modules mixed into this "World"
  my_account.deposit(amount)
end
  • ./features/step\definitions/teller\steps.rb
When /^I request \$(#{CAPTURE_NUMBER})$/ do |amount|
  teller.withdraw_from(my_account, amount)
end
  • ./features/step\definitions/cash\slot\steps.rb
Then /^\$(#{CAPTURE_NUMBER}) should be dispensed$/ do |amount|
  cash_slot.contents.should == amount
end

Last Updated 2015-11-15 Sun 15:57.

Created by Howard Hou with Emacs 24.5.1 (Org mode 8.2.10)