Engine Yard Blog RSS Feed

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!


Tagged:

comments powered by Disqus