Cucumber: More Advanced
In a previous post, I gave you some introductory information on Cucumber, a great framework for writing and executing high level descriptions of your software’s functionality. In this post, I’ll take a deeper dive and talk about a few more advanced Cucumber topics: project structures, multiple language support, scenario tables, free-form stories, tags, hooks and backgrounds. As always, for more detailed information see the documentation and/or The RSpec Book.
Project Structure
Let’s start by taking a look at your project structure: the usual advice is to have a features
directory as the root of your Cucumber work. In that directory, you place all of your .feature
files which contain your features (as you would expect) as well as support
and step_definitions
directories.
The support
directory should contain whatever support code your features need, and an env.rb
file which is responsible for loading any required code that lives outside the feature
directory tree.
In the step_definitions
directory, you place the files (with .rb
extensions) that contain your step definitions. These will all get loaded when your features run.
You can have multiple subdirectories in the features
directory for grouping features. This allows you to run the features in a particular directory. While this can be useful, it can be awkward in practice. That’s because (as of this writing) cucumber
loads each ruby file (ending in .rb
) it finds in the directory you tell it to run and, recursively, all subdirectories. For example, consider the following tree (each .rb
files simply has a puts "x"
where x
is the name of the file):
features
+- 1.rb
+- 2.rb
+- sub1
+- 3.rb
+- sub2
+- 4.rb
+- sub3
+- 5.rb
+- sub4
+- 6.rb
So when you run cucumber features
, you get:
1
2
3
4
5
6
0 scenarios
0 steps
0m0.000s
but, if you run cucumber features/sub2
you get:
4
5
6
0 scenarios
0 steps
0m0.000s
The issue is that features don’t inherit support code or steps from parent directories unless that parent is also visited by Cucumber. So if you want to run subdirectories separately and they share setup code or steps, you have to somehow duplicate that code (possibly by explicitly requiring files from up the tree). This isn’t significant, but it is a bit messy. A better approach might be to use tags (described below). If your features groups don’t share much, then using subdirectories can work fine.
Languages
In the referenced previous post I talked briefly about Cucumber’s multiple language support; here’s I’ll show you how it’s done, and how you can add your own if required.
The file cucumber/lib/cucumber/languages.yml
defines the ‘natural language’ support of Cucumber. This is a yaml file that provides multi-lingual aliases for Gherkin keywords. As an example, here’s the entries for English, LOLZ, and Japanese:
"en":
name: English
native: English
encoding: UTF-8
feature: Feature
background: Background
scenario: Scenario
scenario_outline: Scenario Outline
examples: Examples|Scenarios
given: Given
when: When
then: Then
and: And
but: But
space_after_keyword: true
"en-lol":
name: LOLCAT
native: LOLCAT
encoding: UTF-8
feature: OH HAI
background: B4
scenario: MISHUN
scenario_outline: MISHUN SRSLY
examples: EXAMPLZ
given: I CAN HAZ
when: WEN
then: DEN
and: AN
but: BUT
space_after_keyword: true
"ja":
name: Japanese
native: 日本語
encoding: UTF-8
feature: フィーチャ|機能
background: 背景
scenario: シナリオ
scenario_outline: シナリオアウトライン|シナリオテンプレート|テンプレ|シナリオテンプレ
examples: 例|サンプル
given: 前提
when: もし
then: ならば
and: かつ
but: しかし|但し
space_after_keyword: false
To see what languages are available in the version of Cucumber you have, issue the following command:
cucumber --language help
If your desired language isn’t supported, clone aslakhellesoy/cucumber and add it. If you do, please send aslakhellesoy a pull request to have it considered for inclusion.
To run features written in a specific language, use the --language
flag followed by the 2 (or so) letter abbreviation of the desired language. For example:
cucumber --language ja
Whenever I’m talking about Gherkin keywords, I’ll use the English form. Everything said applies to any language version.
Free-form Story/Text
As mentioned in the intro post, following the feature header, you can have any amount of free-form text. This is generally used to give further background or explanation of the feature. Here’s an example from The RSpec Book:
Feature: code-breaker submits guess
The code-breaker submits a guess of four colored
pegs. The mastermind game marks the guess with black
and white "marker" pegs.
For each peg in the guess that matches color
and position of a peg in the secret code, the
mark includes one black peg. For each additional
peg in the guess that matches the color but not
the position of a color in the secret code, a
white peg is added to the mark.
Scenario Outline: submit guess
...
Scenario Outlines and Example Tables
Sometimes you will have a collection of scenarios that are structurally all the same, differing only in some set of values. It might be nice to represent these as a table. In fact, the FIT project is aimed at doing just this. However, it can be nice to use a single tool to solve a given class of problem, and so Cucumber supports this style of testing.
There are two pieces to this capability. First is the definition of the scenario outline. This is a skeleton of the scenario, written with placeholders for the actual values. Continuing on from the previous feature:
Scenario Outline: submit guess
Given the secret code is `
When I guess <guess>
Then the mark should be <mark>
This is very similar to a regular scenario definition, with two exceptions. First, you use Scenario Outline:
instead of Scenario:
. This is what informs the system that you want to do a tabular style scenario. The second difference is the use of placeholders, e.g. <guess>
.
The second piece is a data table (or tables). These start with the Scenarios:
or Examples:
keyword. Following the keyword is a textual description of the data in the table. Then we have a table header that serves to map the columns to the placeholders in the scenario. Following that is the data for each case the scenario should be applied to, one per line. Table cells are bracketed by vertical bars (i.e |
):
Scenarios: all colors correct
| code | guess | mark |
| r g y c | r g y c | bbbb |
| r g y c | r g c y | bbww |
| r g y c | y r g c | bwww |
| r g y c | c r g y | wwww |
Scenarios: 3 colors correct
| code | guess | mark |
| r g y c | w g y c | bbb |
| r g y c | w r y c | bbw |
| r g y c | w r g c | bww |
| r g y c | w r g y | www |
Scenarios: 2 colors correct
| code | guess | mark |
| r g y c | w g w c | bb |
| r g y c | w r w c | bw |
| r g y c | g w c w | ww |
Scenarios: 1 color correct
| code | guess | mark |
| r g y c | r w w w | b |
| r g y c | w w r w | w |
Any number of scenario tables can be used, and all apply to the most recent scenario outline. This results in far less copy/paste and the associated potential for error, as well as being a far more concise format.
Tags
In the simplest case, Cucumber runs all the scenarios in all the features that you point it at. By using tags you can be more specific about what is run. In my opinion, this is a killer feature. You tag features or scenarios by prefixing them with one or more tags, separated by spaces. A tag is simply an identifier prefixed by @
. For example:
@billing @annoy
Feature: Verify billing
@important
Scenario: Missing product description
Scenario: Several products
To run features and/or scenarios with specific tags you use the --tags
command line flag and provide a comma separated list of tags. Specifying a feature with a tag runs all scenarios in the feature. Here’s a couple of examples based on the above code:
cucumber --tags @billing # Runs both scenarios
cucumber --tags @important # Runs the first scenario
You can also specify tags not to run, like so:
cucumber --tags ~@important # Runs the second scenario (Scenarios without @important)
cucumber --tags ~@important,~@other # Won't run tasks tagged @important or @other.
Hooks
Cucumber provides the ability to supply hooks to modify the behavior of executing features. Hooks can be used at the various levels of granularity, generally before and after, and are often defined in your env.rb file. Hooks are executed whenever the event they are defined for occurs.
###Global Hooks
Global hooks run when Cucumber begins and exits. Begin hooks are informal: simply put the desired code in a file in the features/support
directory (possibly env.rb
, but I’d advise keeping it separate).
An exit hook is more formal, using at_exit
. Here’s an example of a pair of global hooks:
#the begin 'hook'
my_heavy_object = HeavyObject.new
my_heavy_object.do_it
at_exit do
my_heavy_object.undo_it
end
###Scenario Hooks You can add blocks that will run before and after each scenario:
Before do
# Do something before each scenario.
end
After do |scenario|
# Do something after each scenario.
end
The After block will be passed the scenario that just ran. You can use this to inspect its result status by using the failed?
, passed?
and exception
methods. For example:
After do |scenario|
if(scenario.failed?)
subject = " \[Project X\] #{scenario.exception.message}"
send_failure_email(subject)
end
end
###Step Hooks Cucumber gives you the ability to define a block that will be executed after each (and every) step:
AfterStep |scenario| do
# Do something after each step.
end
###Tagged Hooks
If you’ve tagged some of your scenarios, you can also tag scenario and step hooks. You simply pass the tags as arguments to the hook methods). These tagged hooks will only be executed before/after scenarios that are tagged the same and after steps in scenarios tagged the same. Here’s an example where the hooks will only be run before scenarios tagged with @cucumis
or @sativus
.
Before('@cucumis', '@sativus') do
# Do something before scenarios tagged @cucmis or @sativus
end
AfterStep('@cucumis', '@sativus') do
# Do something after steps tagged @cucmis or @sativus
end
Background
A Background
is very much like a scenario in that it consists of a series of steps. The difference is that its steps are executed before the steps of each scenario in the feature. It’s basically a factoring out of a set of common lead-in steps for the features scenarios. One thing to remember is that a Background
is run after any Before
hooks.
You’ll generally only have given Backgrounds. Here’s an example:
Feature: User Login
Background:
Given account 'A123' for 'Dave' with password '123'
And account 'B456' for 'Joe' with password 'abc'
Scenario: Dave logs in and sees his account
When I log in as 'Dave' using password '123'
Then I am in account 'A123'
Scenario: Jow logs in and sees his account
When I log in as 'Joe' using password 'abc'
Then I am in account 'B456'
Summary
So that’s a quick look at some of the more advanced features of Cucumber. It’s a great tool, with a growing community behind and around it. I’ll be posting on Cucumber Best Practices next time, so keep an eye out. Enjoy, and comment with any questions!
Share your thoughts with @engineyard on Twitter