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