现在应该可以把在第一部分学习到的所有知识一起使用了。我们还要解释一些cucumber的高级概念,使用一个例子来解释会更加容易。在这个部分,很多时候会混淆开发者和测试者之间的概念。你如果是一个测试,不要担心:我们使用的ruby代码刚开始是非常简单的。随着不断地深入,你会非常了解它是如何工作的,当然也会学习到很多知识。
4.5节的最后,我们从零开始构建ATM软件。我们有了一个简单的场景,但它是系统最终的行为:有人来到机器前取款。
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的整个世界,如何使用它在step定义之间共享状态。我们还要写一些自定义帮助方法,引入step定义和应用之间的去耦层。我们会向你展示如何使用transform来减少step定义之间的重复,使得它们的正则表达式更加可读。最终,我们想你展示我们是怎么组织文件的,使用和维护这些文件会更加方便。
7.1 绘制领域模型
任何面向对象编程的核心都是领域模型。当我们开始构建一个新系统,我们都会想要直接和领域模型直接工作。这允许我们来快速迭代,分析我们正在工作的问题,不会被用户接口搞得混乱。一旦我们有一个领域模型,能够真正反映我们理解的系统,在一个漂亮的皮肤中封装它是很容易的。
我们将要使得cucumber驱动我们的工作,在step定义中直接构建领域模型类型。通常,我么调用cucumber来提醒我们,下面要做什么:
当我们持续工作在这个场景后,我们会做到:为每个step定义编写了正则表达式,开始完成第一个step。下面是我们的step文件:
Given /Î have deposited \$(\d+) in my account$/ do |amount|
Account.new(amount.to_i)
End
When /Î 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
在第一个step定义中,我们调用了一个假想的Account类。Ruby给我们一个错误,告诉我们下一件事情是要定义Account类。如下操作:
class Account
def initialize(amount)
end
end
Given /Î have deposited \$(\d+) in my account$/ do |amount|
Account.new(amount.to_i)
End
注意:我们在step文件中定义了类。不要担心,它不会永远呆在这儿,但我们工作时,将它创建在这儿是非常方便的。一旦我们有了清晰的思路,可以重构它,移动它到更加持久的家中。我们从step中获取到字符串amount,然后转换成数字,传入到领域模型中。
我们再次运行cucumber:
Feature: Cash Withdrawal
Scenario: Successful withdrawal from an account in credit
Given I have deposited $100 in my account
When I request $20
TODO (Cucumber::Pending)
./features/step_definitions/steps.rb:13
features/cash_withdrawal.feature:4
Then $20 should be dispensed
1 scenario (1 pending)
3 steps (1 skipped, 1 pending, 1 passed)
0m0.002s
太容易了!让我们在自己的step定义中审查代码。还有一些我们不想看到的问题:
1. 这里有一些前后矛盾的语言:这个step讨论存钱到账户中,但是代码却是将钱传入到Account构造函数中;
2. 这个step正在欺骗我们!它说:“Given I have deposited $100 in my account”,并且通过了。但是我们知道,在我们的实现中,什么都没有存储。
3. 非常凌乱地转换amount到整数。如果我们有一个叫amount的变量,我们应该期望它已经是一些类型的数字,而不是通过正则表达式捕获来的字符串。
在我们转到下面的步骤前,我们会干完这些问题。
获取正确的词句
我们需要阐明语法,因此请思考我们如果编写代码,才能让step读起来更像文本。我们应该重新定义这个step:Given an Account with a balance of $100。实际上,账户拥有余额的办法只有让人往里面存钱。因此,让我们在step定义中,改变和领域模型交流的方式:
class Account
def deposit(amount)
end
end
Given /Î have deposited \$(\d+) in my account$/ do |amount|
my_account = Account.new
my_account.deposit(amount.to_i)
end
这样看起来更好了。
这段话中还有一些问题困扰我们。在step中,我们讨论my account,意味着在场景中主人公和这个账户有联系,也许是顾客。这是我们很可能在领域概念中遗漏的符号。然而,即使我们使用这个场景,并且要处理不止一个顾客时,我们都想保持事物简单,专注于设计最少的、只要这个场景能够运行的类型。因此我们停止关心这个场景。
报告事实
我们非常开心地和自己的Account类交互,我们可以解决接下来的代码问题。在我们存钱后,我们需要在断言中核实它的余额。
Given /Î have deposited \$(\d+) in my account$/ do |amount|
my_account = Account.new
my_account.deposit(amount.to_i)
my_account.balance.should eq(amount.to_i),
"Expected the balance to be #{amount} but it was #{my_account.balance}"
End
我们使用RSpec断言,如果你喜欢其它断言库,随意使用。看起来将一个断言放在Given step中很器官,但是它和将来的读者交流时,他们能看出我们期望的系统状态。我们需要添加一个balance方法到Account中,以便于运行这个代码:
class Account
def deposit(amount)
end
def balance
end
end
注意,我们仅仅描绘出类的接口,而没有添加任何实现。这种工作方式是从外向内开发。我们不需要考虑Acount是怎么样工作的,但需要关心它需要怎么样工作。
现在运行测试,我们得到了一个很有帮助的失败信息:
Feature: Cash Withdrawal
Scenario: Successful withdrawal from an account in credit
Given I have deposited $100 in my account
Expected the balance to be 100 but it was
(RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/steps.rb:15
features/cash_withdrawal.feature:3
When I request $20
Then $20 should be dispensed
Failing Scenarios:
cucumber features/cash_withdrawal.feature:2
1 scenario (1 failed)
3 steps (1 failed, 2 skipped)
0m0.002s
现在我们的step定义已经非常健壮,因为我们知道如果它不能存钱到账户,他就会报警。向Given和When step中添加断言意味着:如果项目中有回归会更加容易诊断,因为当问题出现时,场景会失败。当你描述问题时,这个技术非常有用:最终我们移动这个验证到测试单元中,离开step定义。
做最简单的事
在这里,我们有一个决定。我们已经完成第一个step定义,但是我们不能离开这里,因为我们改变了Account类,我们必须让step通过。
暂停并且将Account类移到一个单独的文件中,然后使用单元测试驱动我们想要的行为,这是非常诱人的。我们应该尽力抵制这个诱惑,继续在Account外围工作。如果从这个场景中获得全部的视角,一旦我们实现step时,我们会在类接口设计上更加有信心。
因此,我们编写了Account类的一个简单地、未完成的实现,并且让第一个step通过。这就像在建筑场地放置了一个脚手架:虽然我们想要早最终实现它,但是它会帮助建立一些好东西。
像这样改变Account,第一个步骤就会通过:
class Account
def deposit(amount)
@balance = amount
end
def balance
@balance
end
end
很好。在我们的列表上仍然有一个问题,to_i的复制问题。注意,我们的step通过了,我们可以很有信心的重构。
7.2 在转换中移除复制
这个step定义中另一个问题是我们必须转换正则表达式捕获的字符串到整形。事实上,我们添加了一个断言,我们必须做两次转换。当我们的测试套件逐渐变大,我们可以想象to_i的调用会充斥我们的step定义。即使只有4个字符,我们也要消灭他们。
我们需要学习另一个cucumber方法:Transfrom。
Transform用于捕获参数。每个transfrom负责转换某些字符串,变成更有意义的物体。例如,我们可以使用下面的transform将匹配参数转换成Ruby Fixnum整数:
Transform /^\d+$/ do |number|
number.to_i
end
我们通过正则表达式来定义transform,描述transform感兴趣的参数。注意我们使用^和$来锚点transform的正则表达式来定位捕获字符串的两端。这是很重要的,因为我们想要自己的transform来匹配数字,而不捕获仅仅包含一个数字的字符串。
当cucumber匹配step定义,它验证任意transform来匹配每个参数。当一个参数匹配一个transform,cucumber传输匹配字符串到transform的代码块,代码执行结果就会传送到step定义中。
使用transform来代替,我们可以移除to_i的重复:
Given /Î have deposited \$(\d+) in my account$/ do |amount|
my_account = Account.new
my_account.deposit(amount)
my_account.balance.should eq(amount),
"Expected the balance to be #{amount} but it was #{my_account.balance}"
End
非常好!这个代码看起来更加易读、更加清洁了,虽然引入这个transform给我们带来了另一种形式的重复。我们用来捕获数字的正则表达式也重复了:我们在两个步骤和转换定义中都使用了\d+。这个可能会带来问题,例如,在我们的feature中引入美分:我们必须在step定义和转换中都要改变这个正则表达式。幸运的是:cucumber允许我们一旦在transform中定义正则表达式,就可以在step定义中重用它们。例如:
CAPTURE_A_NUMBER = Transform /^\d+$/ do |number|
number.to_i
end
Given /Î have deposited \$(#{CAPTURE_A_NUMBER}) in my account$/ do |amount|
my_account = Account.new
my_account.deposit(amount)
my_account.balance.should eq(amount),
"Expected the balance to be #{amount} but it was #{my_account.balance}"
End
我们将调用常量中Transform的结果,然后当我们在step定义中建立正则表达式时使用常量。为了更容易修改,方便将来在捕获正则表达式中使用,当转换参数时,这种重构可以使得其他人阅读step定义时更加清晰。
我们可以移动美元符号到transform的捕获中可以让这项工作进行得更彻底。这使得代码更加紧密,因为我们让捕获存款金额的整个正则表达式的语句放在一起。这也给我们捕获其他货币的选择。
CAPTURE_CASH_AMOUNT = Transform /^\$(\d+)$/ do |digits|
digits.to_i
end
Given /Î have deposited (#{CAPTURE_CASH_AMOUNT}) in my account$/ do |amount|
my_account = Account.new
my_account.deposit(amount)
my_account.balance.should eq(amount),
"Expected the balance to be #{amount} but it was #{my_account.balance}"
End
注意:我们在transform内使用了捕获组,在美元符号从字符串中分隔。这样告诉cucumber:我们仅仅对transform中的捕获的部分感兴趣,因此仅仅数字会传送到我们的代码中。如果我们想要捕获美元符号,我们可以将另一个捕获组,这个会作为另一个参数传给Transform代码。
Transform /^(£|\$|€)(\d+)$/ do | currency_symbol, digits |
Currency::Money.new(digits, currency_symbol)
End
让我们再次查看todo列表。使用transform已经清理了初始化代码中的最后一个问题。当我们继续开始时,我们会收集一个新的todo列表:我们需要正确实现Account,使用单元测试。让我们转移到下一个场景step中,现在留下这个问题。
添加自定义帮助方法
我们已经实现了场景中的第一个step,创建了一个有余额的账户,取款应该可以工作了。讨论完transform后,我们很难记住下一步真正需要的工作,但是我们可以依赖cucumber来提醒我们。
Feature: Cash Withdrawal
Scenario: Successful withdrawal from an account in credit
Given I have deposited $100 in my account
When I request $20
TODO (Cucumber::Pending)
./features/step_definitions/steps.rb:28
features/cash_withdrawal.feature:4
Then $20 should be dispensed
1 scenario (1 pending)
3 steps (1 skipped, 1 pending, 1 passed)
0m0.002s
第一个步骤正如我们期望地通过了,第二个失败了,通知了一个pending消息。因此,我们接下来的工作是实现这个step来仿真从ATM机上取款。下面是空的step定义:
When /Î request \$(\d+)$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
首先我们重用transform,转换现金数量为数值:
When /Î request (#{CAPTURE_CASH_AMOUNT})$/ do |amount|
pending # express the regexp above with the code you wish you had
end
我们需要从account中取款,意味着我们需要为这个场景带来一个新的行动。这个新类会处理我们的需求,从我们的账户中提取现金。在现实中,如果我们去银行,这个角色就会有teller扮演。再次从外部思考,让我们这样描述代码:
When /Î request (#{CAPTURE_CASH_AMOUNT})$/ do |amount|
teller = Teller.new
teller.withdraw_from(my_account, amount)
end
看起来很好。Teller需要知道从那个账户取钱,取多少钱。这里又有一些不一致进入了语言中:step定义讨论查询现金,但是在这个代码中我们在取款。取款是我们常用的属于,因此让我们修改场景中的文本匹配原本的意义。
Feature: Cash Withdrawal
Scenario: Successful withdrawal from an account in credit
Given I have deposited $100 in my account
When I withdraw $20
Then $20 should be dispensed
When /Î withdraw (#{CAPTURE_CASH_AMOUNT})$/ do |amount|
teller = Teller.new
teller.withdraw_from(my_account, amount)
end
这更好了。现在场景中的语言更加真实地反应了代码想要表达的意思。运行cucumber,你应该被提示创建teller类。让我们来创建一个:
class Teller
def withdraw_from(account, amount)
end
end
我们实现了接口,没有添加实现,再次运行cucumber:
Feature: Cash Withdrawal
Scenario: Successful withdrawal from an account in credit
Given I have deposited $100 in my account
When I withdraw $20
undefined local variable or method ‘my_account’ for
#<Object:0x63756b65> (NameError)
./features/step_definitions/steps.rb:32
features/cash_withdrawal.feature:4
Then $20 should be dispensed
Failing Scenarios:
cucumber features/cash_withdrawal.feature:2
1 scenario (1 failed)
3 steps (1 failed, 1 skipped, 1 passed)
0m0.003s
我们在第一个step定义中定义了my_accout,但是我们想要在第二个step定义中使用它,但ruby看不见它。如何才能让两个step定义都看到呢?答案隐藏在一些基础结构下面:需要知道cucumber如何运行step定义。
在World中保存状态
在执行一个场景钱,cucumber创建一个新项目。我们称它为world。场景中Step定义在world的上下文中执行,尽管他们都是world对象的方法。就想一个普通的ruby类的方法,我们可以在step定义中使用实例变量传递状态。
下面是代码演示如何在step定义中,将my_account作为一个实例变量:
Given /Î have deposited (#{CAPTURE_CASH_AMOUNT}) in my account$/ do |amount|
@my_account = Account.new
@my_account.deposit(amount)
@my_account.balance.should eq(amount), "Expected the balance to be #{amount}"
end
When /Î withdraw (#{CAPTURE_CASH_AMOUNT})$/ do |amount|
teller = Teller.new
teller.withdraw_from(@my_account, amount)
end
它正常工作了。
解决方案可以了,但是我们不喜欢在step定义中这样使用实例变量。实例变量的问题是:如果你不设置他们,他们就会返回nil。我们讨厌nil,因为他们侵入你的系统,导致怪异的bug,很难找到。例如,后来有人进入项目组,使用第二个step定义,但他们没有设置@my_account,他们会传入一个nil到Teller#withdraw_from。
我们向你展示一个快速重构,避免在step定义中定义实例变量,在一个helper方法中创建账户。
创建自定义帮助方法
在常规类中,在访问方法中设置一个实例变量就可以避免nil,如下:
def my_account
@my_account ||= Account.new
End
使用||=保证:新Account仅仅创建一次,然后保存在实例变量中。你可以在cucumber做相同的事情。向world中添加一个自定义方法,在模块中定义他们,然后告诉cucumber你想要他们加入到你的world中。如下:
module KnowsMyAccount
def my_account
@my_account ||= Account.new
end
end
我们在KnowsMyAccount模块中定义my_account,然后告诉cucumber,通过调用world方法插入这个模块到我们的world中。通常,到目前为止,我们已经完成所有问题,但是我们还要清理一下它们。
这意味着我们可以去除初始化Account的代码,并且去除实例变量,因此step定义重新使用my_account:
Given /Î have deposited (#{CAPTURE_CASH_AMOUNT}) in my account$/ do |amount|
my_account.deposit(amount)
my_account.balance.should eq(amount),
"Expected the balance to be #{amount} but it was #{my_account.balance}"
end
When /Î withdraw (#{CAPTURE_CASH_AMOUNT})$/ do |amount|
teller = Teller.new
teller.withdraw_from(my_account, amount)
end
这一次my_account不会使用局部变量,而是调用我们的帮助方法。运行cucumber,一切正常。
自定义world
我们第二个步骤通过了,让我们暂停一会,学习一些关于world的知识。
使用world的主要方式是使用模块代码来扩展它,根据你的系统执行常用行为来支持你的step定义:
module MyCustomHelperModule
def my_helper_method
...
end
end
World(MyCustomHelperModule)
在这个情况下,你会扩展world对象。默认情况下,cucumber通过调用Object.new方式创建一个World,但是你可以重载这个,使用你需要的另一个类,只需要传递一个block到World方法:
class MySpecialWorld
def some_helper_method
# …
end
end
World { MySpecialWorld.new }
不管你用什么类当做你的world,cucumber经常混入Cucumber::RbSupport::RbWorld,包含不同的帮助方法,你可以使用来和cucumber交流。Pending方法就是这样的例子。本书中说明了很多方法,如果你想要完整的手册,最好查找Cucumber::RbSupport::RbWorld的API手册。
你可以查找所有混和的模块,通过在step定义中调用puts self。因为cucumber和RSpec是好朋友,如果你使用RSpec gem,你会发现cucumber自动混入Rspec::Matchers。
记住新的world是为所有的场景创建的,因此他会在场景结束时释放。这就帮助了隔离场景,因为任何实例变量都会在场景结束时销毁。
在结束时设计我们的方式
回到真实世界,我们想要最后的step通过。当我们运行cucumber时,我们可以看到前两个step通过了,最终的一个是pending。最后一步的step定义是:
Then /^\$(\d+) should be dispensed$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
最大的问题是:现金应该分发到哪儿呢?系统的那个部分可以让我们确信,系统是否有余款?看起来好像我们丢了一个领域概念。在物理ATM中,现金会从ATM的一个槽中吐出来,就像这样:
Then /^(#{CAPTURE_CASH_AMOUNT}) should be dispensed$/ do |amount|
cash_slot.contents.should == amount
end
看起来很好。当我们将代码绑定到真实硬件上时,我们还需要一些方式和它交流,现在这个对象工作得很好。让我们运行cucumber来驱动它。首先我们需要一个cash_slot方法。让我们添加这个方法到我们的帮助模块中,重命名这个模块来反应它的角色。
module KnowsTheDomain
def my_account
@my_account ||= Account.new
end
def cash_slot
@cash_slot ||= CashSlot.new
end
end
World(KnowsTheDomain)
再次运行cucumber,这次需要定义CashSlot类。我们再次构建一个接口简图,仅有最少的实现:
class CashSlot
def contents
raise("I'm empty!")
end
end
现在运行cucumber,我们已经非常接近我们的目标:所有的类和方法都联络完成,最后一步失败是因为没有cash出来。
Feature: Cash Withdrawal
Scenario: Successful withdrawal from an account in credit
Given I have deposited $100 in my account
When I withdraw $20
Then $20 should be dispensed
I’m empty! (RuntimeError)
./features/step_definitions/steps.rb:19
./features/step_definitions/steps.rb:55
features/cash_withdrawal.feature:5
Failing Scenarios:
cucumber features/cash_withdrawal.feature:2
1 scenario (1 failed)
3 steps (1 failed, 2 passed)
0m0.004s
让最后一步通过,有些人想要通知CashSlot来分发现金。Teller是事务的主导人,但现在他不知道任何关于CashSlot的事情。我们使用依赖caruso来传递CashSlot到Teller的构造器中。现在我们假定一个新CashSlot方法:Teller可以使用它来告诉ATM分发现金:
class Teller
def initialize(cash_slot)
@cash_slot = cash_slot
end
def withdraw_from(account, amount)
@cash_slot.dispense(amount)
end
end
这个看起来是Teller的最简单实现。当我们从外面设计方法时,我们认为我们需要一个account参数,但现在我们看起来不许它,这似乎很器官。让我们来关注他,虽然:我们写了一个标注,专注于让这个测试通过。
这里有两个变动,我们需要添加dispense方法到CashSlot中,我们必须改变第二个step定义来正确创建Teller:
When /Î withdraw (#{CAPTURE_CASH_AMOUNT})$/ do |amount|
teller = Teller.new(cash_slot)
teller.withdraw_from(my_account, amount)
end
这个调用现在似乎不合适了,所有我们的类都在World的帮助方法中创建,因此修改如下:
module KnowsTheDomain
def my_account
@my_account ||= Account.new
end
def cash_slot
@cash_slot ||= CashSlot.new
end
def teller
@teller ||= Teller.new(cash_slot)
end
end
World(KnowsTheDomain)
这意味着我们的step定义少了很多的混乱:
When /Î withdraw (#{CAPTURE_CASH_AMOUNT})$/ do |amount|
teller.withdraw_from(my_account, amount)
end
这个step定义代码读起来非常好。下推一些细节到我们的World中意味着step定义代码在更高的抽象层次上。当你进入step定义时,不需要太多的经历,因为代码不会包含太多的细节。
除非我们在CashSlot上做一些工作,否则这个场景不会通过。运行cucumber,你会看到丢失dispense方法的消息。添加一个简单的实现:
class CashSlot
def contents
@contents or raise("I'm empty!")
end
def dispense(amount)
@contents = amount
end
end
最后一次运行cucumber,你应该能够看到这个场景通过了。
Feature: Cash Withdrawal
Scenario: Successful withdrawal from an account in credit
Given I have deposited $100 in my account
When I withdraw $20
Then $20 should be dispensed
1 scenario (1 passed)
3 steps (3 passed)
0m0.002s
非常好!休息一下吧,审查代码,做一些重构。
4. 组织代码
在Ruby工程的lib目录中保存系统代码是一个惯例。我们需要为应用程序定义一个名称,因为另一个惯例是:程序的进入点是一个以你应用程序命名的文件,这个文件存在lib文件夹中。公司尽力向人们突出这个商标:NiceBank,因此创建lib/nice_bank.rb,移动三个类Account、Teller、CashSlots到这里。
现在用下面这行代码替换三个类的代码,用来加载lib目录下的文件:
require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'nice_bank')
保存文件,运行cucumber。
虽然工作了,但不是最好的。甚至在cucumber开始查看step定义前,加载应用代码可以让我们的测试正确运行。幸运的是,cucumber给我们一个特殊的文件夹做这个事情。
启动cucumber环境
当第一次启动cucumber时,在加载step定义前,会加载一个support文件夹中的文件。因为我们的例子很简单,到目前为止我们还没有用到这个目录。Support目录用来存放支持step定义的代码,就可以使得step定义更加简单。
就像features/step_definitions,cucumber会加载support中的ruby文件。这是约定,因为这意味着你不需要在任何地方插入require语句,但是并不意味着某个文件的命令很难被控制。实际上,这意味着你不能在support目下的两个中创建依赖,因为你不能预期谁会被先加载。这里有一个异常情况,env.rb
Env.rb文件一直都是cucumber启动一个测试时,加载的第一个文件。你可以使用它来准备其他support的环境和step定义代码。加载自己的应用程序是最基本的,因此移动require语句到你的env.lib文件中
Transform和World模块
我们应该清理transform和world模块。创建一个叫support/transforms.rb,将transform移动到这个文件中。像这样保持transform是很有用的,因为我们正在改变他们,我们可以阅读其他的transform保证所有都是一致的。运行cucumber,都可以通过。
我们将KnowsTheDomain模块和调用World的调用移动到support/world_extension.rb文件中。当我们添加更多的方法到world中,我们可以将他们分成分入多个模块中,每一个在各自的文件中;现在这个文件布局已经很好了。
组织我们的step定义
我们使用一个叫steps.rb的文件放置step定义,因为项目太小,我们在很长时间内都会使用一个单独的文件。当step定义逐渐增多时,我们会分割成多个文件,以便于代码更加一致。我们找到了最合适的方式组织step定义文件,为每个领域实体创建一个文件。因此我们有三个文件:
features/step_definitions/account_steps.rb
features/step_definitions/teller_steps.rb
features/step_definitions/cash_slot_steps.rb
这些写文件仅仅包含一个step定义,现在我们有足够的空间来增长。
Dry run and env.rb
当我们开始移动文件时,测试所有功能依然匹配是很重要的,使用cucumber –dry-run。一个dry run的目标是解析你的feature和step定义,但是并不实际运行任何一个。这比运行一个真实的测试快得多,如果你仅仅想要打印出你的feature,或者使用usage格式查看未定义的step。
Dry运行和正常运行最大的不同时:dry运行不会启动你的环境,因此env.rb不会加载。意味着你需要小心的摆放你的文件:
1. 确认其他在support的文件可以在没有env.rb存在的情况下加载
2. 将所有缓慢的代码放入env.rb,这样dry运行就会更快。
开始使用dry运行,尤其是使用usage格式,可以帮助你更有信心地重构step定义和场景。
我们学到了什么
看起来我们构建的系统仅仅是一个小玩具。没有任何用户可交互的具体组件:我们的CashSlot仅仅是一个简单的ruby类,没有任何的按钮供用户使用。虽然我们只有一个初始状态的领域模型,但是对问题有了深刻的理解。从外向里看并不意味着从用户接口开始:它意味着从你外部任何你发现的东西开始。
我们知道有时是不可能的。你要经常添加测试到系统中,或者在用户接口已经被定义完成的项目上编写cucumber代码。甚至在这些情况下,尽力使用ruby类为你的领域建模会帮助你理解,在较长时间内使得代码更容易维护。
以下是我们在本章讨论的内容:
1. 通过删除枯燥的重复代码,使用Transform来帮助可维护性。他们也可以为你的正则表达式的最有用部分设定名称。
2. 支持step定义的代码可以放置到features/support文件夹,这里的文件都在step定义前被加载;
3. Support/env.rb首先被加载,你可以在这里启动你的应用程序。这个文件不会在dry run中启动。
4. 为每个领域实体定义一个文件来组织step定义是很好的实践。
5. Step定义在一个叫world的对象中执行。你可以使用实例变量在steps中传递状态,也可以在ruby模块中混入帮助方法。
通过添加我们自己的world模块,我们可以让step定义更加可读,我们已经开始对step定义去耦合。去耦的好处会在系统成长是体现好处。事实上,下一章我们向你展示如何引入一个web用户接口来取款,不需要改变step定义中的一行代码。
尝试
这里有很多地方,你可以使得这个例子运行起来。这里有一个建议:
更大的重写:
现在我们发现了领域模型,为什么不看看能能在做一次呢?除了features、Transform,删除所有的文件。然后删除每个step定义主体,改变它为pending,关闭本书,运行cucumber。
尽量忘记我们已经做得,享受发现一个领域模型过程的快乐。
追踪错误
还有一个需要我们做的问题,提醒我们:我们需要调用为什么起初设计Teller#withdraw_from方法时使用两个参数,而我们仅仅使用其中一个。
查看你是否指出这个不一致意味着什么。考虑如何修改解决这个问题。尽力解决这个问题,我们将在下一章的开始讨论这个话题,因此如果你不确定答案,你没有太长时间等待。
边界测试
通过取款的过程,我们有了一个简单的场景。你能想象在这个场景上做一些简单的变化,就能导致不同的输出吗?例如,如果你的账户钱很少,怎么办?
在相同的features中写出你的场景,如果你想要挑战,尝试自动化他们。