Nesting Steps

Finding the right level of detail, or abstraction, to use in your scenarios is a skill that takes some time to master. What many people don’t realize is that different levels of detail are appropriate for different scenarios in the same system—sometimes in the same feature—depending on what it is they’re describing.

As an example, here’s a scenario for the user of our ATM authenticating with their PIN:

 
Scenario:​ Successful login with PIN​
 
Given ​I have pushed my card in the slot​
 
When ​I enter my PIN​
 
And ​I press ​"OK"​​
 
Then ​I should see the main menu

It’s entirely appropriate for us to go into this much detail about the authentication process in this scenario, because that’s where our focus is. Now consider our cash withdrawal scenario from earlier, which has a different focus but still needs to be authenticated. Does it make sense to express the PIN authentication steps of this scenario at the same level of detail? Let’s try it:

 
Scenario:​ Withdraw fixed amount of $50​
 
Given ​I have $500 in my account​
 
And ​I have pushed my card into the slot​
 
And ​I enter my PIN​
 
And ​I press ​"OK"​​
 
When ​I choose to withdraw the fixed amount of $50​
 
Then ​I should receive $50 cash​
 
And ​the balance of my account should be $450

That’s awful! There’s so much noise about authentication that we hardly notice the important part: the part about withdrawing cash. That detail was useful in the PIN scenario where it was relevant, but now it’s just distracting. We’ll talk more about the dangers of over-detailed or imperative scenarios in Chapter 6, When Cucumbers Go Bad; right now we want to show you a simple refactoring to push these details down into the step definitions so our scenario is easy to read.

Nest Steps Refactoring

Let’s take the three authentication steps and summarize what they do with a single high-level step:

 
Given ​I have authenticated with the correct PIN

Cut the three lines from the scenario into your clipboard, and replace them with that single step. Now run cucumber to generate the step definition snippet for your new high-level step. It should look like this:

 
Given /^I have authenticated with the correct PIN$/ ​do
 
pending ​# express the regexp above with the code you wish you had
 
end

Now, use Cucumber’s built-in steps method to call out to the original three steps in the body of your high-level step’s definition:

 
Given /^I have authenticated with the correct PIN$/ ​do
 
steps ​%{
 
And I have pushed my card into the slot
 
And I enter my PIN
 
And I press "OK"
 
}
 
end

The %{ ... } construct is just a way to tell Ruby that you have a string going across multiple lines. When Cucumber runs this high-level step, it will delegate to each of the lower-level steps. So, the behavior of the scenario is exactly the same, but the language in the business-facing feature is at a higher level.

Arguments and Nested Steps

If the low-level steps you are nesting within a new high-level step take arguments, you’ll want to be able to capture those arguments in the high-level step and pass them to the low-level steps. Since the steps method just takes a string, you can use Ruby’s string interpolation to pass in the arguments:

 
Given /^an activated customer (w+) exists$/ ​do​ |name|
 
steps ​%{
 
Given I create a customer with login ​#{name}
 
And I register the customer with login ​#{name}
 
And I activate the customer with login ​#{name}
 
}
 
end

The only time this won’t work is if the argument you’re passing is a data table. In that case you can use another method, step to call a specific step, passing the actual typed DataTable object as an argument:

 
Given /^a (w+) widget with the following details:$/ ​do​ |color, details_table|
 
step ​"I create a ​#{color}​ widget with the following details:"​, details_table
 
steps ​%{
 
And I register the ​#{color}​ widget
 
And I activate the ​#{color}​ widget
 
}
 
end

Nested steps work best when they’re simple: a high-level step delegating to a few lower-level ones. As soon as you start adding arguments into the mix, you’re already starting to make things more complicated than they probably need to be. Remember that underneath it all there are Ruby methods being called. Often it makes sense to use the Ruby methods directly rather than obscuring them with layers of nested steps.

The Dangers of Nested Steps

Nested steps are a useful tool to reach for when you need to quickly refactor an overly detailed scenario to make it more readable. However, you need to watch out: they can lure you down a path to complexity and frustration. Here’s a story from Andrew Premdas, an experienced Cucumber user who once was a big fan of nested steps:

Don't Use Nested Steps
by Andrew Premdas
Andrew Premdas

Nested steps are a temptation. They beguile you with the idea that you can achieve something valuable with great ease. But this is a mirage, because anything you do with nested steps can be done a simpler way by using Ruby code instead.

When implementing step definitions, what is important is to make it crystal clear exactly what you are doing so you can be sure your scenario does what it says, and not something different. Nested steps allow you to implement step definitions without really knowing what you are doing or how you are doing it. They put unnecessary layers of obscurity between the scenario and the code that actually does the work.

Nesting steps prevents you from becoming a better programmer. The programming involved in implementing step definitions is generally quite simple. Every time you use a nested step you lose an opportunity to interact directly with the underlying code that talks to your system and understand how it works. Instead of becoming empowered and learning with each new scenario, you become more and more dependent on nested steps. As your use of nested steps increases, this dependency increases, as does the complexity of your step implementations.

The important thing to remember is this: you can apply the extract method refactoring to the body of every step definition. This choice is always preferable to nesting because it’s always easier to find, read, and work with a Ruby method call than another step definition. Why bother nesting when you can extract methods instead?

On our project that used nested steps, the more we nested, the harder it became to implement scenarios—they just kept breaking for really strange reasons. Getting rid of the nested steps made things much more transparent. Our colleagues noticed the improvement immediately and soon were telling us off for nesting steps.

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

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