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