力香: 守护!自在! 读书!

导航

丰富的场景

丰富的场景

现在应该可以把在第一部分学习到的所有知识一起使用了。我们还要解释一些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中写出你的场景,如果你想要挑战,尝试自动化他们。

posted on 2016-03-08 11:18  力香  阅读(253)  评论(0编辑  收藏  举报