Adding Custom Helper Methods to the World

We’ve implemented the first step of our scenario to set up an account with a sufficient balance that the withdrawal should work. After all that talking about transforms, it’s hard to remember exactly what we need to do next, but we can always rely on cucumber to remind us where we are:

 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.018s

The first step is passing as we’d expect, and the second one is failing with a pending message. So, our next task is to implement the step to simulate a customer withdrawing cash from the ATM. Here’s the empty step definition:

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

First we can reuse the transform we created earlier to translate the cash amount, captured as a string by Cucumber, into a number:

 When(​/^I request (​​#{​CAPTURE_CASH_AMOUNT​}​​)$/​) ​do​ |amount|
  pending ​# express the regexp above with the code you wish you had
 end

We need somewhere to withdraw the cash from, which means we need to bring a new actor into this scenario. This new class is going to handle our request to withdraw cash from our account. If we walked into a bank in real life, that role would be played by a teller. Thinking outside-in again, let’s sketch out the code we’d like:

 When(​/^I request (​​#{​CAPTURE_CASH_AMOUNT​}​​)$/​) ​do​ |amount|
  teller = Teller.new
  teller.withdraw_from(my_account, amount)
 end

That looks pretty good. The teller will need to know which account to take the cash from and how much to take. There’s a little inconsistency creeping into the language again, though: the step definition talks about requesting the cash, but in the code we’re withdrawing it. Withdraw is the term we use most often, so let’s change the text in the scenario to match.

 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(​/^I withdraw (​​#{​CAPTURE_CASH_AMOUNT​}​​)$/​) ​do​ |amount|
  teller = Teller.new
  teller.withdraw_from(my_account, amount)
 end

That’s better. Now the language in the scenario more closely reflects what’s going on in the code beneath it. Run cucumber again, and you should be prompted to create a Teller class. Let’s create one:

 class​ Teller
 def​ withdraw_from(account, amount)
 end
 end

Again, we’ve just sketched out the interface for now, without adding any implementation. With that in place, try running cucumber again:

 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:36
  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.024s

A-ha. We defined my_account in the first step definition, but when we tried to use it in the second step definition, Ruby can’t see it. How can we make my_account available to both step definitions? The answer lies in understanding something fundamental about how Cucumber executes step definitions.

Storing State in the World

Just before it executes each scenario, Cucumber creates a new object. We call this object the World. The step definitions for the scenario execute in the context of the World, as though they were methods of that object. Just like methods on a regular Ruby class, we can use instance variables to pass state between step definitions.

Here’s how the code looks with my_account stored as an instance variable in the step definition:

 Given(​/^I 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(​/^I withdraw (​​#{​CAPTURE_CASH_AMOUNT​}​​)$/​) ​do​ |amount|
  teller = Teller.new
  teller.withdraw_from(@my_account, amount)
 end

Try it. It works!

This solution is OK, but we don’t really like leaving instance variables in step definitions like this. The problem with instance variables is that, if you don’t set them, they just return nil. We hate nils, because they creep around your system, causing weird bugs that are hard to track down. For example, if someone were to later come along and use the second step definition in another scenario where they hadn’t already set @my_account, they’d end up passing a nil into Teller#withdraw_from.

We’re going to show you a quick refactoring to get rid of the instance variable from the step definition, by pushing the code that creates the Account into a helper method.

Creating Custom Helper Methods

In a regular class we’d avoid nils by putting the instance variables behind an accessor method, like this:

 def​ my_account
  @my_account ||= Account.new
 end

Using ||= ensures that the new Account will be created only once and then stored in an instance variable. You can do the same in Cucumber. To add custom methods to the World, you define them in a module and then tell Cucumber you want them to be mixed into your World. Here’s what we need to do:

 module​ KnowsMyAccount
 def​ my_account
  @my_account ||= Account.new
 end
 end
 
 World(KnowsMyAccount)

We defined my_account on a module called KnowsMyAccount and then told Cucumber to mix that module into our World by calling Cucumber’s special World method. As usual, we’ve done all this inline in the step definition file for now, but we’re going to tidy it away soon.

This means we can get rid of the code that initializes the Account from the first step definition, and we can get rid of the instance variable, so the step definitions go back to using my_account again:

 Given(​/^I have deposited (​​#{​CAPTURE_CASH_AMOUNT​}​​) in my account$/​) ​do​ |amount|
  my_account.deposit(amount)
  expect(my_account.balance).to eq(amount),
 "Expected the balance to be ​​#{​amount​}​​ but it was ​​#{​my_account.balance​}​​"
 end
 
 When(​/^I withdraw (​​#{​CAPTURE_CASH_AMOUNT​}​​)$/​) ​do​ |amount|
  teller = Teller.new
  teller.withdraw_from(my_account, amount)
 end

This time my_account won’t use a local variable in the step definition, but it will call our helper method instead. With this change in place, run cucumber, and everything should be passing.

Customizing the World

Our second step is passing now, so let’s take a break from the example to learn a bit more about the World.

The main way to use the World is by extending it with modules of code that support your step definitions by performing common actions against your system:

 module​ MyCustomHelperModule
 def​ my_helper_method
  ...
  end
 end
 
 World(MyCustomHelperModule)

In this case, you’ll be extending a World object that Cucumber has created for you. By default, Cucumber creates each World by simply calling Object.new, but you can override this and use a different class if you need to, by passing a block to the World method:

 class​ MySpecialWorld
 def​ some_helper_method
 # …
 end
 end
 
 World { MySpecialWorld.new }

Whatever class you use for your worlds, Cucumber always mixes in the module Cucumber::RbSupport::RbWorld, which contains various helper methods you can use to talk to Cucumber from your step definitions. The method pending that we’ve used a few times already in the book is an example of this. There are explanations of many of these methods scattered throughout the book, but if you want a complete reference of them, the best thing to do is look up the API documentation[34] for Cucumber::RbSupport::RbWorld.

You can discover all the modules that are mixed into the world by calling puts self from within a step definition. A list of modules will be printed to the output. And because Cucumber and RSpec are friends, you’ll find that Cucumber automatically mixes in RSpec’s RSpec::Matchers if it can see that you’re using the RSpec gem.

Remember that a new world is created for each scenario, so it is destroyed at the end of the scenario. This helps isolate scenarios from one another, because any instance variables set during one scenario will be destroyed along with the world in which they were created when the scenario ends.

Designing Our Way to the Finish Line

Back in the real world, we were trying to get our final step to pass. When we run cucumber, we can see that the first two steps are passing, and the final one is pending. Almost there! Let’s take a look at that last step definition:

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

The big question here is: where will the cash be dispensed? Which part of the system can we examine for evidence of whether it doled out the readies? It seems as if we’re missing a domain concept. In the physical ATM, the cash will end up poking out of a slot on the ATM, something like this:

 Then(​/^(​​#{​CAPTURE_CASH_AMOUNT​}​​) should be dispensed$/​) ​do​ |amount|
  expect(cash_slot.contents).to eq(amount)
 end

That looks good. When we hook our code up to the real hardware, we’re going to need some way of talking to it, and this object will work fine as a test double in the meantime. Let’s start running cucumber to drive this out. First it tells us that we need to define the cash_slot method, of course. Let’s add that method to our helper module and rename the module to reflect its new role.

 module​ KnowsTheDomain
 def​ my_account
  @my_account ||= Account.new
 end
 
 def​ cash_slot
  @cash_slot ||= CashSlot.new
 end
 end
 
 World(KnowsTheDomain)

We run cucumber again, and this time it wants us to define the CashSlot class, so let’s go ahead and do as we’re told. Again, we just build a sketch of the interface we want to use, with minimal implementation:

 class​ CashSlot
 def​ contents
 raise​(​"I'm empty"​)
 end
 end

Now when you run cucumber, you’ll see we’ve moved closer to our goal: all the classes and methods are wired up, and our final step is failing just because there’s no cash coming out of the CashSlot:

 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.023s

To get this last step to pass, someone needs to tell the CashSlot to dispense the cash when the customer makes a withdrawal. It’s the Teller who’s in charge of the transaction, but at the moment he doesn’t know anything about the CashSlot. We’ll use dependency injection to pass the CashSlot in to Teller’s constructor. Now we can imagine a new CashSlot method that the Teller can use to tell it to dispense the cash:

 class​ Teller
 def​ initialize(cash_slot)
  @cash_slot = cash_slot
 end
 
 def​ withdraw_from(account, amount)
  @cash_slot.dispense(amount)
 end
 end

This seems like the simplest implementation of Teller that we need to get the scenario to pass. It’s odd that when we designed this method from the outside we thought we’d need the account parameter, but now we don’t seem to need it. Let’s stay focused, though: we’ll make a note on our to-do list to look into this later and carry on getting this step to pass.

There are two changes we need to make now. We need to add the new dispense method to CashSlot, and we have to change the second step definition to create the Teller correctly:

 When(​/^I withdraw (​​#{​CAPTURE_CASH_AMOUNT​}​​)$/​) ​do​ |amount|
  teller = Teller.new(cash_slot)
  teller.withdraw_from(my_account, amount)
 end

This call to Teller.new seems out of place in a step definition now. All our other classes are created inside helper methods in the World, so let’s do the same thing here:

 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)

This means our step definition becomes a lot less cluttered:

 When(​/^I withdraw (​​#{​CAPTURE_CASH_AMOUNT​}​​)$/​) ​do​ |amount|
  teller.withdraw_from(my_account, amount)
 end

The step definition code all reads very nicely now. Pushing some of the details down into our World module means the step definition code is at a higher level of abstraction. This makes it less of a mental leap when you come into the step definitions from a business-facing scenario, because the code doesn’t contain too much detail.

This scenario isn’t going to pass until we do some work on our CashSlot, though. Run cucumber, and you’ll see that it’s missing the dispense method. A simple implementation of CashSlot should get this working:

 class​ CashSlot
 def​ contents
  @contents or ​raise​(​"I'm empty"​)
 end
 
 def​ dispense(amount)
  @contents = amount
 end
 end

Run cucumber one last time, and you should see the scenario pass:

 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.021s

Excellent! Go and grab a drink—then we can sit down, review the code, and do some refactoring.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.144.56