© Panos Matsinopoulos 2020
P. MatsinopoulosPractical Test Automationhttps://doi.org/10.1007/978-1-4842-6141-5_7

7. Advanced Cucumber

Panos Matsinopoulos1 
(1)
KERATEA, Greece
 

In the previous chapter, I gave you an introduction to Cucumber. In this chapter, you learn about the more advanced features of Cucumber, and you also go into implementing a more elaborate example case. There is a lot to cover in this chapter, including the extensive test case, making the chapter a little bit longer than the average.

Learning Goals

  1. 1.

    Learn about the Feature keyword.

     
  2. 2.

    Learn about the Background keyword.

     
  3. 3.

    Learn about the Scenario keyword.

     
  4. 4.

    Learn about the different logical phases a scenario needs to be divided into.

     
  5. 5.

    Learn about the And and But keywords.

     
  6. 6.

    Learn about the importance of the Scenario name and description.

     
  7. 7.

    Learn about the state of Scenarios.

     
  8. 8.

    Learn about data tables.

     
  9. 9.

    Learn about the internationalization features of Cucumber.

     
  10. 10.

    Learn how to use a test-first approach by practicing a more complex Cucumber project.

     
  11. 11.

    Learn how you can use the Cucumber environment files to set up Cucumber using the application code.

     
  12. 12.

    Learn how and when to use RSpec alongside Cucumber.

     
  13. 13.

    Learn about running a dry run.

     
  14. 14.

    Learn how you can invoke a single Feature file, when you have multiple ones in your project.

     
  15. 15.

    Learn how to invoke a single Scenario and skip all the others defined in the same or other Feature file.

     
  16. 16.

    Learn how to invoke a specific Example out of a set of Examples of a Scenario Outline.

     
  17. 17.

    Learn how to invoke the execution of more than one specific Scenario.

     
  18. 18.

    Learn how you can use tags to tag specific Scenarios and then invoke or exclude their execution.

     
  19. 19.

    Learn how you can execute specific Ruby code at specific points in the lifecycle of the execution of a Scenario.

     
  20. 20.

    Learn about the document strings.

     
  21. 21.

    Learn how well RubyMine supports Cucumber.

     

Introduction

In the previous chapter, you had your first encounter with Cucumber and Gherkin. Let’s now see some more advanced features. You will now better understand the Gherkin language.

Gherkin Keywords

The language that you use to write your features is called Gherkin. Being a language, it means that you have a specific set of language keywords that you can use. This is the list of them:
  • Feature

  • Background

  • Scenario

  • Given

  • When

  • Then

  • And

  • But

  • *

  • Scenario Outline

  • Examples

Let’s see how these keywords work.

Feature

The Feature is used to give a descriptive name and a longer summary description to your set of Scenarios. Otherwise, it does not affect your test/executable specification.

When you start your Feature, you give a title and a summary description. The title is separated from the keyword Feature with a :. The summary description is a multiline text below the title and needs to be indented by two blanks to the right. Note that the lines in the summary description should not start with any of the words Scenario, Background, or Scenario Outline. This is because these three keywords denote the start of other special blocks in the feature file.

In the following, you can see an example of a feature with name/title Invite a Friend and some summary description:
Feature: Invite a Friend
  User is able to invite a friend.
  Inviting friends will bring more users to the platform and will increase invitee user reputation.
  Moreover, invited friends will get a discount for signing up
  ...

Sometimes a feature is quite simple. But there are times in which a feature is complex. In this case, you need to write some extra documentation to reason for the value the feature will bring. This is the purpose of the summary description. It is optional, but if you believe that it will bear useful business value information, it would be good to be there.

Background

The Background groups together the steps that need to take place before each scenario that is part of the feature at hand. In other words, if many of your Scenarios share a common start that is repeated on all Scenarios, you may want to transfer those steps in the Background area.

Here is a Feature with two Scenarios that share a common start:
Feature: Update My Profile Picture
  The user updates their profile picture
  So that their picture appears on all public and private pages next to their name.
  Background:
    Given I am signed in as a standard user
    And I visit my profile section
  Scenario: Uploading a new picture
    When I click on my current profile picture
    Then I can see an area on which I can drop my new profile picture
  Scenario: Deleting the current profile picture
    When I click on delete profile picture button
    Then I see that my picture is replaced with a default one
The preceding feature content is equivalent to the one here:
Feature: Update My Profile Picture
  The user updates their profile picture
  So that their picture appears on all public and private pages next to their name.
  Scenario: Uploading a new picture
    Given I am signed in as a standard user
    And I visit my profile section
    When I click on my current profile picture
    Then I can see an area on which I can drop my new profile picture
  Scenario: Deleting the current profile picture
    Given I am signed in as a standard user
    And I visit my profile section
    When I click on delete profile picture button
    Then I see that my picture is replaced with a default one

But you prefer the first version, with the Background, in order to avoid cluttering the main Scenario content with steps that are not really relevant to the actual value of the Scenario.

Scenario

You learned about the Scenario keyword using some examples, both in the previous and in the current chapter. What I would like to underline here is the fact that usually, a Scenario is decomposed into three logical parts/phases (Figure 7-1).
../images/497830_1_En_7_Chapter/497830_1_En_7_Fig1_HTML.jpg
Figure 7-1

Scenario – Three Phases

  1. 1.

    You prepare the data in your application so that you have the correct state ready for testing. This is what I also call context.

     
  2. 2.

    You fire the event/action that you want to test.

     
  3. 3.

    You check that the new state of your application data is as expected.

     
In Gherkin, you use the keyword Given to identify steps that are part of the first phase, the context preparation. You use the keyword When to identify steps that are part of the second phase, that of firing the action. You use the keyword Then to identify steps that are part of the third phase (Figure 7-2).
../images/497830_1_En_7_Chapter/497830_1_En_7_Fig2_HTML.jpg
Figure 7-2

Gherkin Keywords Mapping to Three Scenario Phases

For example, see the following Scenario:
Scenario: Deleting the current profile picture
  Given I am signed in as a standard user
  And I visit my profile section
  When I click on delete profile picture button
  Then I see that my picture is replaced with a default one

The first two steps are part of the first phase, the preparation of the context. You could have started both with the Given keyword, but Gherkin allows you to use And and But to avoid repeating Given multiple times, one below the other.

The third step starts with a When, and it is the firing action. This is the action that changes the state of the system. Usually, this is one step only, but this is not a hard rule.

The fourth step starts with a Then, and it is the step that checks that the system now has the correct state.

When writing your scenarios, make sure that you clearly identify these three phases in your scenario wording. It helps the reader to understand what the Scenario is trying to specify and test.

And, But, and *

I said that you can use And and But to start your step invocations inside a Scenario. Also, if you find that these keywords add too much of a verbosity to the Scenarios, then you can always use * instead – or a mixture of these. That wouldn’t make any difference to cucumber and the way it would be executing your Scenarios.

See the following example:
Scenario: Attempt to withdraw with an invalid card
  Given I have $100 in my account
      * my card is invalid
   When I request $50
   Then my card should not be returned
      * and I should be asked to contact the bank

It is using a mixture of the Gherkin keywords. It clearly delineates the phases of the Scenario by using the correct keyword at the start of the phase and * for steps in the middle of them.

Scenario Names and Description

As a Feature has a name/title and a description, the same goes for a Scenario. Scenarios should have descriptive names/titles, but they should not be very long. You can always add a multiline summary description to a Scenario as long as the lines of that description do not start with any of the reserved step invocation keywords Given, When, and Then.

Note again that the summary descriptions should not be developer oriented and should not include technical implementation details. They are meant to be read by the business stakeholders and the developers at the same time. So avoid any technical jargon. They need to elaborate on the business aspect of the Scenario they brief about.

Scenario State

Although within the execution of a Scenario you can use instance variables to carry out state from one step to the next, please note that these instance variables do not survive from one Scenario to the next. This is done by design for a good reason. The Scenarios should be independent to each other. Each Scenario should be executable on its own, and its execution should not be dependent on the execution results of the previous Scenarios.

Having said that, make sure that you don’t carry over state from one Scenario to the next using any kind of persistent storage. For example, do not persist state into the file system or into a database, expecting to find it ready to be accessed in the next Scenario. This is really a bad design practice.

Data Tables

Sometimes, when you prepare the data of a Scenario, you end up repeating the same step with different runtime arguments multiple times. See the following example:
Scenario: Export a CSV file of the customers
  Given the customer "John Woo" with contact phone number "6972669700"
  And the customer "Mary Foo" with contact phone number "6972669701"
  And the customer "Laura Bar" with contact phone number '6972669702"
  When I request to export customers details
  Then I get the file "customers.csv" with the correct data

You can see that the first three lines of the preceding Scenario are invocations on the same step with different data. Do you have another way to declare these, so that they would have been less verbose and easier to read?

The answer is Cucumber data tables.

Let’s create a project called customers, integrate Cucumber, and write the following content into a .feature file (Listing 7-1).
# File: customers/features/export_to_csv.feature
#
Feature: Export to CSV
  Scenario: Export standard CSV file for our customers
    Given the customers
      | name      | phone number |
      | John Woo  | 6972669700   |
      | Mary Foo  | 6972669701   |
      | Laura Bar | 6972669702   |
    When I request to export customer details
    Then I get the file "customers.csv" with the correct data
Listing 7-1

Export to CSV Feature

This Scenario bears the same information as the following Scenario:
Scenario: Export a CSV file of the customers
  Given the customer "John Woo" with contact phone number "6972669700"
  And the customer "Mary Foo" with contact phone number "6972669701"
  And the customer "Laura Bar" with contact phone number '6972669702"
  When I request to export customers details
  Then I get the file "customers.csv" with the correct data

However, it is much easier to read and actually maintain.

Let’s run the cucumber executable. This is what you get:
$ bundle exec cucumber --format progress
UUU
1 scenario (1 undefined)
3 steps (3 undefined)
0m0.007s
You can implement step definitions for undefined steps with these snippets:
Given("the customers") do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
end
When("I request to export customer details") do
  pending # Write code here that turns the phrase above into concrete actions
end
Then("I get the file {string} with the correct data") do |string|
  pending # Write code here that turns the phrase above into concrete actions
end
$
All steps are undefined, of course, and Cucumber is suggesting to you which snippets you could start from. Let’s create the steps file features/step_definitions/customers_steps.rb (Listing 7-2).
# File: customers/features/step_definitions/customers_steps.rb
#
Given("the customers") do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
end
When("I request to export customer details") do
  pending # Write code here that turns the phrase above into concrete actions
end
Then("I get the file {string} with the correct data") do |string|
  pending # Write code here that turns the phrase above into concrete actions
end
Listing 7-2

customers_steps.rb File

The interesting thing here is that Cucumber has identified that the step the customers takes its input from a table, rather than interpolated. Having done that, it is proposing you the block local variable table, and also it is telling you that the table is of type Cucumber::MultilineArgument::DataTable, the documentation of which can be found here: http://www.rubydoc.info/gems/cucumber/Cucumber/MultilineArgument/DataTable.

How can you use this table variable to have access to the table data specified in the Scenario? One of the most interesting and useful instance methods of this object is the method #hashes which converts this table into an array of Hash where the keys of each Hash are the headers in the table.

Let’s use this to get access to the customers data, that is, to build the context that the Scenario needs (Listing 7-3).
# File: customers/features/step_definitions/customers_steps.rb
#
Given("the customers") do |table|
  @customers = []
  table.hashes.each do |hash|
    @customers << Customer.new(name: hash['name'], phone_number: hash['phone_number'])
  end
end
When("I request to export customer details") do
  pending # Write code here that turns the phrase above into concrete actions
end
Then("I get the file {string} with the correct data") do |string|
  pending # Write code here that turns the phrase above into concrete actions
end
Listing 7-3

Using the table Variable and hashes Method

Do you see how I implement the first step? I iterate over the table.hashes array. Each item of the array is a Hash that has as keys the column headers of the table and values the values of the corresponding row of the table. Hence, the first item has the values of the first row, the second has the values of the third row, and so on. I take advantage of this, and I create the @customers instance variable, which I plan to use in the steps that follow.

If you run cucumber again, you will get an error for the Customer constant, but this is how test-first development works, isn’t it?

You will not progress to the implementation of this project. You are working another project in more detail later on in this chapter.

Spoken Languages

You will be surprised to learn that Cucumber allows you to write your Features and Scenarios in more than 40 different spoken languages. That can be a real buy-in when you try to convince your business stakeholders to work together with you on specifying the requirements of the application using Gherkin.

Which Languages?

You can tell which languages your Cucumber installation supports by running the command cucumber --i18n-languages . You will get something like this:
$ cucumber --i18n-languages
 | af        | Afrikaans           | Afrikaans             |
 | ast       | Asturian            | asturianu             |
 | az        | Azerbaijani         | Azərbaycanca          |
...
 | uz        | Uzbek               | Узбекча               |
...
 | zh-TW     | Chinese traditional |

The first two-digit code is important if you want to tell that a Feature file is written in a particular language.

Specify the Language of a Feature File

In order for you to tell Cucumber that the language of the Feature is not English (which is the default language), then you need to use the Ruby comment # language: <language-two-digit-code> at the first line of your Feature file.

For example, this is a Feature written in Spanish:
# language: es
Característica: Retirar dinero
  Escenario: Intentar retirar con una tarjeta no válida
        Dado Tengo $100 en mi cuenta
           * Mi tarjeta no es válida
      Cuando Solicito $50
    Entonces Mi tarjeta no debe ser devuelta
* Y se me pedirá que se ponga en contacto con el banco

Localization of Gherkin Keywords

And how do you know which Gherkin keywords should be used for a non-English language? You can get those by invoking the command line cucumber --i18n-keywords <language-code>. For example, this is how you get back the keywords for the Spanish language:
$ cucumber --i18n-keywords es
  | feature          | "Característica"
  | background       | "Antecedentes"
  | scenario         | "Escenario"
  | scenario_outline | "Esquema del escenario"
  | examples         | "Ejemplos"
  | given            | "* ", "Dado ", "Dada ", "Dados ", "Dadas "
  | when             | "* ", "Cuando "
  | then             | "* ", "Entonces "
  | and              | "* ", "Y ", "E "
  | but              | "* ", "Pero "
  | given (code)     | "Dado", "Dada", "Dados", "Dadas"
  | when (code)      | "Cuando"
  | then (code)      | "Entonces"
  | and (code)       | "Y", "E"
  | but (code)       | "Pero"
$

A More Extensive Example

Before you finish with Cucumber, let’s try a more extensive example.

Here is the high-level description of the application that you want to build:

You own a hotel with rooms for booking. You want to implement a new application that would allow you to offer bookings online. The main feature of the application is going to be the booking feature.

Initialize the Project

Initialize the project for the application development. Let’s call it booking-with-joy.

Make sure that you
  1. 1.

    Create the .ruby-version file at the root of your project folder. This file should contain the version number of the Ruby that you want to use, for example, 2.6.6. That way, rbenv and RubyMine know which Ruby will be used for your project.

     
  2. 2.

    Create the RubyMine project.

     

Gemfile

Create the Gemfile and add references to cucumber and rspec. Then bundle to bring the gems in (Listing 7-4).
# File: Gemfile
#
source 'https://rubygems.org'
gem 'cucumber'
gem 'rspec'
Listing 7-4

Gemfile for the booking-with-joy Project

Initialize Cucumber

Execute the following command to initialize Cucumber:
$ cucumber --init
  create   features
  create   features/step_definitions
  create   features/support
  create   features/support/env.rb
$

The Feature File

Let’s start with the following feature file. You only have one Scenario, as a start (Listing 7-5).
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel
      | number | accommodates |
      |      1 |            2 |
      |      2 |            2 |
      |      3 |            2 |
      |      4 |            2 |
      |      5 |            4 |
      |      6 |            4 |
  Scenario: All the rooms are free and there are rooms on the capacity requested
    Given all the rooms are available
    When visitor provides the following booking details
      | check_in    | check_out   | guests |
      | 23-Mar-2021 | 25-Mar-2021 | 3      |
    Then visitor is provided with the following options for booking
      | number |
      |      5 |
      |      6 |
Listing 7-5

Feature with One Scenario

I will not expand on what this Scenario specifies as a functional requirement of the application. This is the beauty of Gherkin. You can read the Scenario itself and understand what it is about. Maybe you want to take a note that I have decided to put in the Background section, the setup of the context of the particular Scenario. This is because I want to keep in the Scenario the steps that are relevant to the value it brings and not distract the reader with unnecessary noise.

Undefined Steps

Let’s run cucumber:
$ cucumber --format progress
UUUU
1 scenario (1 undefined)
4 steps (4 undefined)
0m0.136s
You can implement step definitions for undefined steps with these snippets:
Given("there are the following rooms in the hotel") do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
end
Given("all the rooms are available") do
  pending # Write code here that turns the phrase above into concrete actions
end
When("visitor provides the following booking details") do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
end
Then("visitor is provided with the following options for booking") do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
end
$
All steps are undefined, and you’ve got back the snippets that you can use to start implementing them. Let’s create the step definitions file and put these snippets in (Listing 7-6).
# File features/step_definitions/booking_with_joy_steps.rb
#
Given("there are the following rooms in the hotel") do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
end
Given("all the rooms are available") do
  pending # Write code here that turns the phrase above into concrete actions
end
When("visitor provides the following booking details") do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
end
Then("visitor is provided with the following options for booking") do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
end
Listing 7-6

Initial Content for the Steps File

If you run the cucumber command again, you will get this:
$ cucumber --format progress
P---
(::) pending steps (::)
features/step_definitions/booking_with_joy_steps.rb:3:in `"there are the following rooms in the hotel"'
1 scenario (1 pending)
4 steps (3 skipped, 1 pending)
0m0.123s
$

This is telling you that the first step is pending, whereas the rest of the steps have been skipped.

Implementing the First Step

The first step could have an implementation like the following (Listing 7-7).
Given("there are the following rooms in the hotel") do |table|
  @rooms = []
  table.hashes.each do |hash|
    @rooms << Room.new(number: hash['number'].to_i, accommodates: hash['accommodates'].to_i)
  end
end
Listing 7-7

First Step Implementation

It is building an array of the available rooms, based on the information borne into the table given in the step invocation.

If you run cucumber again, you will get an error about the Room constant being uninitialized:
$ cucumber --format progress
F---
(::) failed steps (::)
uninitialized constant Room (NameError)
./features/step_definitions/booking_with_joy_steps.rb:6:in `block (2 levels) in <top (required)>'
./features/step_definitions/booking_with_joy_steps.rb:5:in `each'
./features/step_definitions/booking_with_joy_steps.rb:5:in `"there are the following rooms in the hotel"'
features/booking_a_room.feature:13:in `Given there are the following rooms in the hotel'
Failing Scenarios:
cucumber features/booking_a_room.feature:22 # Scenario: All the rooms are free and there are rooms on the capacity requested
1 scenario (1 failed)
4 steps (1 failed, 3 skipped)
0m0.175s
$
Let’s start implementing our application by introducing the class Room. You will build this class so that it makes the first step pass successfully. Create the file room.rb, at the root folder of your project, with the following content (Listing 7-8).
# File: room.rb
#
class Room
  def initialize(number:, accommodates:)
    @number = number
    @accommodates = accommodates
  end
end
Listing 7-8

Room Class Definition

If you run cucumber again, you will get the same error, although you have defined your Room class. I guess this is expected because cucumber does not know anything about this class.

Let’s create the file that requires all the application files. Name the file all.rb and create it with the following content in the root folder of your project (Listing 7-9).
# File: all.rb
#
$LOAD_PATH.unshift('.')
require 'room'
Listing 7-9

all.rb Requires All Necessary Files

Let’s make cucumber require this file. Edit the features/support/env.rb so that it requires the all.rb file of your application (Listing 7-10).
# File: features/support/env.rb
#
APPLICATION_ROOT_PATH = File.join(File.expand_path('..', __FILE__), '..', '..')
$LOAD_PATH.unshift(APPLICATION_ROOT_PATH)
require 'all'
Listing 7-10

env.rb File Requires all.rb

Now, everything is ready for the first step to pass successfully. Let’s run cucumber again:
$ cucumber --format progress
.P--
(::) pending steps (::)
features/step_definitions/booking_with_joy_steps.rb:10:in `"all the rooms are available"'
1 scenario (1 pending)
4 steps (2 skipped, 1 pending, 1 passed)
0m0.126s
$

Implementing the Second Step

The second step requires that you set all the rooms in an available state. Let’s implement the step like the following (Listing 7-11).
Given("all the rooms are available") do
  @rooms.each(&:set_available)
end
Listing 7-11

Second Step Implementation

If you run cucumber again, you will get this:
$ cucumber --format progress
.F--
(::) failed steps (::)
undefined method `set_available' for #<Room:0x00007f986d8c6970 @number=1, @accommodates=2> (NoMethodError)
./features/step_definitions/booking_with_joy_steps.rb:11:in `each'
./features/step_definitions/booking_with_joy_steps.rb:11:in `"all the rooms are available"'
features/booking_a_room.feature:23:in `Given all the rooms are available'
Failing Scenarios:
cucumber features/booking_a_room.feature:22 # Scenario: All the rooms are free and there are rooms on the capacity requested
1 scenario (1 failed)
4 steps (1 failed, 2 skipped, 1 passed)
0m0.169s
$
The second step fails because the method set_available is not part of the Room class interface. Let’s implement this (Listing 7-12).
# File: room.rb
#
class Room
  def initialize(number:, accommodates:)
    @number = number
    @accommodates = accommodates
  end
  def set_available
    @state = :available
  end
end
Listing 7-12

Implementing the Method set_available

The method set_available flags the room as available. Now, let’s run the cucumber command again:
$ cucumber --format progress
..P-
(::) pending steps (::)
features/step_definitions/booking_with_joy_steps.rb:14:in `"visitor provides the following booking details"'
1 scenario (1 pending)
4 steps (1 skipped, 1 pending, 2 passed)
0m0.164s
$

Perfect! The second step has now passed.

Implementing the Third Step

The third step is about the visitor providing their booking request details. As you did with room details, you are going to create the step to hold the booking request details. Here is the content of the features/step_definitions/booking_with_joy_steps.rb with the implementation of the step (Listing 7-13).
# File features/step_definitions/booking_with_joy_steps.rb
#
require 'date'
Given("there are the following rooms in the hotel") do |table|
  @rooms = []
  table.hashes.each do |hash|
    @rooms << Room.new(number: hash['number'].to_i, accommodates: hash['accommodates'].to_i)
  end
end
Given("all the rooms are available") do
  @rooms.each(&:set_available)
end
When("visitor provides the following booking details") do |table|
  # Note that "table" will only hold 1 line/row. The whole table
  # refers to 1 instance of a booking request
  hash = table.hashes.first
  @booking_request = BookingRequest.new(
    check_in: Date.strptime(hash['check_in'], '%d-%b-%Y'),
    check_out: Date.strptime(hash['check_out'], '%d-%b-%Y'),
    guests: hash['guests'].to_i
  )
  @available_rooms = @hotel.check_availability(@booking_request)
end
Then("visitor is provided with the following options for booking") do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
end
Listing 7-13

Implementation of the Third Step

Caution

You have added the statement require 'date' at the beginning of the file. This is necessary because the implementation is using the Ruby standard library class Date (read about it here: https://ruby-doc.org/stdlib-2.6.6/libdoc/date/rdoc/Date.html).

As you can read from the comment in the step implementation, the table variable will always have one row, referring to just one instance of the BookingRequest class .

Obviously, if you run the cucumber executable now, you will get an error about the missing constant BookingRequest:
$ cucumber --format progress
..F-
(::) failed steps (::)
uninitialized constant BookingRequest (NameError)
./features/step_definitions/booking_with_joy_steps.rb:20:in `"visitor provides the following booking details"'
features/booking_a_room.feature:24:in `When visitor provides the following booking details'
Failing Scenarios:
cucumber features/booking_a_room.feature:22 # Scenario: All the rooms are free and there are rooms on the capacity requested
1 scenario (1 failed)
4 steps (1 failed, 1 skipped, 2 passed)
0m0.107s
$
Let’s introduce this class (Listing 7-14).
# File booking_request.rb
#
class BookingRequest
  def initialize(check_in:, check_out:, guests:)
    @check_in = check_in
    @check_out = check_out
    @guests = guests
  end
end
Listing 7-14

Introducing Class BookingRequest

You need to make sure this file is required. So let’s amend the file all.rb (Listing 7-15).
# File: all.rb
#
$LOAD_PATH.unshift('.')
require 'room'
require 'booking_request'
Listing 7-15

all.rb Requires booking_request.rb

Let’s run cucumber again:
$ cucumber --format progress
..F-
(::) failed steps (::)
undefined method `check_availability' for nil:NilClass (NoMethodError)
./features/step_definitions/booking_with_joy_steps.rb:27:in `"visitor provides the following booking details"'
features/booking_a_room.feature:24:in `When visitor provides the following booking details'
Failing Scenarios:
cucumber features/booking_a_room.feature:22 # Scenario: All the rooms are free and there are rooms on the capacity requested
1 scenario (1 failed)
4 steps (1 failed, 1 skipped, 2 passed)
0m0.066s
$
The third step is still failing. Now, it is because of calling the check_availability method on something that is nil. It is this line of step implementation that is failing:
@available_rooms = @hotel.check_availability(@booking_request)

The instance variable @hotel is representing your booking management object, the hotel, and needs to be instantiated somehow.

Where do you instantiate this? There are quite some good points in the testing code where you can do this. Let’s take a simple approach to instantiate it here, in this step that you need it. So you enhance the step implementation to be as follows (Listing 7-16).
When("visitor provides the following booking details") do |table|
  # Note that "table" will only hold 1 line/row. The whole table
  # refers to 1 instance of a booking request
  hash = table.hashes.first
  @booking_request = BookingRequest.new(
    check_in: Date.strptime(hash['check_in'], '%d-%b-%Y'),
    check_out: Date.strptime(hash['check_out'], '%d-%b-%Y'),
    guests: hash['guests'].to_i
  )
  @hotel = Hotel.new
  @available_rooms = @hotel.check_availability(@booking_request)
end
Listing 7-16

Instantiating @hotel

Obviously, now, if you run the cucumber executable, you will get an error that the Hotel constant is not initialized:
$ cucumber --format progress
..F-
(::) failed steps (::)
uninitialized constant Hotel (NameError)
./features/step_definitions/booking_with_joy_steps.rb:27:in `"visitor provides the following booking details"'
features/booking_a_room.feature:24:in `When visitor provides the following booking details'
Failing Scenarios:
cucumber features/booking_a_room.feature:22 # Scenario: All the rooms are free and there are rooms on the capacity requested
1 scenario (1 failed)
4 steps (1 failed, 1 skipped, 2 passed)
0m0.043s
$
Let’s define this constant in the file hotel.rb (Listing 7-17).
# File: hotel.rb
#
class Hotel
end
Listing 7-17

Defining Constant Hotel in the hotel.rb File

Require the file inside all.rb (Listing 7-18).
# File: all.rb
#
$LOAD_PATH.unshift('.')
require 'room'
require 'booking_request'
require 'hotel'
Listing 7-18

all.rb Requires hotel.rb

And let’s run cucumber again. I am reminding you that you are following a BDD approach to developing your application:
$ cucumber --format progress
..F-
(::) failed steps (::)
undefined method `check_availability' for #<Hotel:0x00007f87f38a1ad8> (NoMethodError)
./features/step_definitions/booking_with_joy_steps.rb:28:in `"visitor provides the following booking details"'
features/booking_a_room.feature:24:in `When visitor provides the following booking details'
Failing Scenarios:
cucumber features/booking_a_room.feature:22 # Scenario: All the rooms are free and there are rooms on the capacity requested
1 scenario (1 failed)
4 steps (1 failed, 1 skipped, 2 passed)
0m0.043s
$
Now the error has to do with the undefined method check_availability for the Hotel instance. Let’s implement this method too (Listing 7-19).
# File: hotel.rb
#
class Hotel
  def check_availability(booking_request)
  end
end
Listing 7-19

Implementing the check_availability Method

Let’s run cucumber again:
$ cucumber --format progress
...P
(::) pending steps (::)
features/step_definitions/booking_with_joy_steps.rb:31:in `"visitor is provided with the following options for booking"'
1 scenario (1 pending)
4 steps (1 pending, 3 passed)
0m0.049s
$

Nice! It looks like the third step is now implemented. Let’s proceed to the next step.

Implementing the Fourth Step

In the final step, you need to test that the available rooms presented to the visitor are the ones expected and given by the step invocation inside the feature file. The Scenario says that the available rooms should be the rooms 5 and 6.

The following is the implementation of this step (Listing 7-20).
Then("visitor is provided with the following options for booking") do |table|
  expect(@available_rooms.size).to eq(table.hashes.size)
  @available_rooms.each_with_index do |available_room, index|
    expect(available_room.number).to eq(table.hashes[index]['number'].to_i)
  end
end
Listing 7-20

Implementation of the Fourth Step

You make sure that the @available_rooms variable holds the correct information, both in terms of number of rooms and in terms of specific rooms.

Let’s run cucumber now:
$ cucumber --format progress
...F
(::) failed steps (::)
undefined method `size' for nil:NilClass (NoMethodError)
./features/step_definitions/booking_with_joy_steps.rb:32:in `"visitor is provided with the following options for booking"'
features/booking_a_room.feature:27:in `Then visitor is provided with the following options for booking'
Failing Scenarios:
cucumber features/booking_a_room.feature:22 # Scenario: All the rooms are free and there are rooms on the capacity requested
1 scenario (1 failed)
4 steps (1 failed, 3 passed)
0m0.048s
$

The step is failing because you are calling size on something that is nil. It is the @available_rooms.size that is raising the error. @available_rooms is nil because the method #check_availability does not return anything.

You need to work more on the implementation of the #check_availability method. Let’s see a better implementation that would make more sense for the step to succeed (Listing 7-21).
# File: hotel.rb
#
class Hotel
  def initialize(rooms:)
    @rooms = rooms
  end
  def check_availability(booking_request)
    existing_available_rooms.select do |existing_available_room|
      existing_available_room.accommodates >= booking_request.guests
    end
  end
  private
  attr_reader :rooms
  def existing_available_rooms
    rooms.select { |room| room.available? }
  end
end
Listing 7-21

Correct Hotel Class Implementation

The preceding implementation requires the hotel rooms to be given when constructing the Hotel instance. So let’s enhance the previous step too, the one that instantiates the Hotel class. Instead of @hotel = Hotel.new, it needs to be @hotel = Hotel.new(rooms: @rooms) since @rooms holds the rooms of the hotel, as instantiated in the first step. Do this change in the file features/step_definitions/booking_with_joy_steps.rb before proceeding.

Also, the new Hotel class implementation will require that the Room instance responds to the method #available? (see the preceding line 19). You will also need the public readers for @accommodates and @number instance variables. Let’s implement these changes too, inside the room.rb file (Listing 7-22).
# File: room.rb
#
class Room
  attr_reader :accommodates, :number
  def initialize(number:, accommodates:)
    @number = number
    @accommodates = accommodates
  end
  def set_available
    @state = :available
  end
  def available?
    @state == :available
  end
end
Listing 7-22

New Room Implementation

Finally, for the BookingRequest instances, you need the public reader for the @guests instance variable. Add the line attr_reader :guests, inside the class BookingRequest (file booking_request.rb ).

You are all set. Let’s run the cucumber executable again:
$ cucumber --format progress
1 scenario (1 passed)
4 steps (4 passed)
0m0.049s
$

Perfect! All the steps run successfully, and the Scenario is green.

Scenario Outline

Let’s introduce some more examples in the same Scenario logic but with different data. In this scenario, it seems that the check-in and checkout dates are not relevant, since all the rooms are initially free. So you will write a Scenario Outline so that you can check the availability depending on the number of guests requested only.

Here is how you turn the current Scenario to a Scenario Outline that would allow you to try different numbers of guests (Listing 7-23).
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel
      | number | accommodates |
      |      1 |            2 |
      |      2 |            2 |
      |      3 |            2 |
      |      4 |            2 |
      |      5 |            4 |
      |      6 |            4 |
  Scenario Outline: All the rooms are free and there are rooms on the capacity requested
    Given all the rooms are available
    When visitor provides the following booking details
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"
    Examples:
      | guests | rooms |
      |      3 | 5, 6  |
Listing 7-23

Using Scenario Outline

The preceding code changes the Scenario to Scenario Outline and introduces a new step visitor is provided with rooms "<rooms>". Then you provide the examples list, with a single example for the time being, the example that corresponds to three guests and the expected rooms 5 and 6 given in a comma-separated list. Hence, this single example corresponds to the original Scenario that you had.

Let’s run cucumber:
$ cucumber --format progress
...U
1 scenario (1 undefined)
4 steps (1 undefined, 3 passed)
0m0.071s
You can implement step definitions for undefined steps with these snippets:
Then("visitor is provided with rooms {string}") do |string|
  pending # Write code here that turns the phrase above into concrete actions
end
$
The new step is undefined, of course. It is easy to implement like the following (Listing 7-24).
Then("visitor is provided with rooms {string}") do |rooms_list|
  expected_room_numbers = rooms_list.split(',').map {|room_number| room_number.to_i}
  expect(@available_rooms.size).to eq(expected_room_numbers.size)
  @available_rooms.each_with_index do |available_room, index|
    expect(available_room.number).to eq(expected_room_numbers[index])
  end
end
Listing 7-24

Undefined Step Definition

You take the room_list, and you build the array with the expected room numbers (expected_room_numbers). Then comparing this array to the @available_rooms is easy and similar to what you did earlier (in the previous step definition).

Now, let’s run cucumber again:
$ cucumber --format progress
1 scenario (1 passed)
4 steps (4 passed)
0m0.055s
$

Great! The feature is green, and one scenario is defined and passed.

But now, the Scenario Outline allows you to test other guests' cases. See this new version of the Examples section (Listing 7-25).
Examples:
  | guests | rooms            |
  |      1 | 1, 2, 3, 4, 5, 6 |
  |      2 | 1, 2, 3, 4, 5, 6 |
  |      3 |             5, 6 |
  |      4 |             5, 6 |
  |      5 |                  |
  |      6 |                  |
  |      7 |                  |
Listing 7-25

Examples Section with More Examples

If you run cucumber again, you will get this:
$ cucumber --format progress
7 scenarios (7 passed)
28 steps (28 passed)
0m0.063s
$

Nice!

Some Rooms Are Not Available

The preceding scenarios assume that all rooms are available. Let’s expand to assume that some of the rooms are not available (Listing 7-26).
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel
      | number | accommodates |
      |      1 |            2 |
      |      2 |            2 |
      |      3 |            2 |
      |      4 |            2 |
      |      5 |            4 |
      |      6 |            4 |
  Scenario Outline: All the rooms are free and there are rooms on the capacity requested
    Given all the rooms are available
    When visitor provides the following booking details
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"
    Examples:
      | guests | rooms            |
      |      1 | 1, 2, 3, 4, 5, 6 |
      |      2 | 1, 2, 3, 4, 5, 6 |
      |      3 |             5, 6 |
      |      4 |             5, 6 |
      |      5 |                  |
      |      6 |                  |
      |      7 |                  |
  Scenario Outline: Some rooms are not available
    Given the rooms "<reserved_rooms>" are reserved
    When visitor provides the following booking details
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"
    Examples:
      | reserved_rooms   | guests | rooms         |
      | 1                | 1      | 2, 3, 4, 5, 6 |
      | 1, 2             | 1      | 3, 4, 5, 6    |
      | 1, 2, 3          | 1      | 4, 5, 6       |
      | 1, 2, 3, 4       | 1      | 5, 6          |
      | 1, 2, 3, 4, 5    | 1      | 6             |
      | 1, 2, 3, 4, 5, 6 | 1      |               |
Listing 7-26

Assuming Some Rooms Are Not Available

As you can see in the preceding code, you have compiled a new Scenario Outline that has some scenarios that include reserved rooms. If you run cucumber, you will get six new scenarios being undefined:
$ cucumber --format progress
.............................U--.U--.U--.U--.U--.U--
13 scenarios (6 undefined, 7 passed)
52 steps (12 skipped, 6 undefined, 34 passed)
0m0.070s
You can implement step definitions for undefined steps with these snippets:
Given("the rooms {string} are reserved") do |string|
  pending # Write code here that turns the phrase above into concrete actions
end
$
Let’s implement the new step (Listing 7-27).
Given("the rooms {string} are reserved") do |rooms_list|
  reserved_rooms = rooms_list.split(',').map(&:to_i)
  reserved_rooms.each { |room_number| @hotel.reserve_room(room_number) }
end
Listing 7-27

Implementation of the Undefined Step

The step calls #reserve_room on the @hotel instance to render the particular room unavailable. However, the @hotel instance is not instantiated, and if you run cucumber, you get undefined method 'reserve_room' for nil:NilClass (NoMethodError). Why? You thought you had it instantiated, but the instantiation was done inside a step that is no longer called in this new Scenario Outline. It’s better if you move the line @hotel = Hotel.new(rooms: @rooms) to be in the Background step, which is called for all scenarios. Let’s do that (Listing 7-28).
# File features/step_definitions/booking_with_joy_steps.rb
#
require 'date'
Given("there are the following rooms in the hotel") do |table|
  @rooms = []
  table.hashes.each do |hash|
    @rooms << Room.new(number: hash['number'].to_i, accommodates: hash['accommodates'].to_i)
  end
  @hotel = Hotel.new(rooms: @rooms)
end
Given("all the rooms are available") do
  @rooms.each(&:set_available)
end
When("visitor provides the following booking details") do |table|
  # Note that "table" will only hold 1 line/row. The whole table
  # refers to 1 instance of a booking request
  hash = table.hashes.first
  @booking_request = BookingRequest.new(
    check_in: Date.strptime(hash['check_in'], '%d-%b-%Y'),
    check_out: Date.strptime(hash['check_out'], '%d-%b-%Y'),
    guests: hash['guests'].to_i
  )
  @available_rooms = @hotel.check_availability(@booking_request)
end
Then("visitor is provided with the following options for booking") do |table|
  expect(@available_rooms.size).to eq(table.hashes.size)
  @available_rooms.each_with_index do |available_room, index|
    expect(available_room.number).to eq(table.hashes[index]['number'].to_i)
  end
end
Then("visitor is provided with rooms {string}") do |rooms_list|
  expected_room_numbers = rooms_list.split(',').map {|room_number| room_number.to_i}
  expect(@available_rooms.size).to eq(expected_room_numbers.size)
  @available_rooms.each_with_index do |available_room, index|
    expect(available_room.number).to eq(expected_room_numbers[index])
  end
end
Given("the rooms {string} are reserved") do |rooms_list|
  reserved_rooms = rooms_list.split(',').map(&:to_i)
  reserved_rooms.each { |room_number| @hotel.reserve_room(room_number) }
end
Listing 7-28

New Content of the Step Implementation File

As you can see in the preceding code, you have added line 10
@hotel = Hotel.new(rooms: @rooms)

and you have removed the instantiation of the @hotel variable from the When(/^visitor provides the following booking details$/) implementation.

Nevertheless, now, if you run cucumber, you get the error undefined method 'reserve_room' for #<Hotel:0x007fd34d84c240> (NoMethodError), which is expected, since the method #reserve_room has not been defined for instances of the Hotel class.

Let’s fix this. Here is the new content of the hotel.rb file (Listing 7-29).
# File: hotel.rb
#
class Hotel
  def initialize(rooms:)
    @rooms = rooms
  end
  def check_availability(booking_request)
    existing_available_rooms.select do |existing_available_room|
      existing_available_room.accommodates >= booking_request.guests
    end
  end
  def reserve_room(room_number)
    @rooms.select {|r| r.number == room_number}.first.reserve
  end
  private
  attr_reader :rooms
  def existing_available_rooms
    rooms.select { |room| room.available? }
  end
end
Listing 7-29

New Content of the hotel.rb File

See the new lines, 14–16. This is where you define the method #reserve_room for the instances of the Hotel class. If you run cucumber again, you will get errors like undefined method 'reserve' for #<Room:0x007fcd2389dfd0 @number=1, @accommodates=2> (NoMethodError). This is because you are calling #reserve on Room instances. But this class does not expose such method.

Let’s implement this method. The following is the new content of the room.rb file (Listing 7-30).
# File: room.rb
#
class Room
  attr_reader :accommodates, :number
  def initialize(number:, accommodates:)
    @number = number
    @accommodates = accommodates
  end
  def set_available
    @state = :available
  end
  def available?
    @state == :available
  end
  def reserve
    @state = :reserved
  end
end
Listing 7-30

New Content of the room.rb File

See the implementation of the method #reserve in between lines 19 and 21. Now, let’s run cucumber:
$ cucumber --format progress
...............................F...F...F...F...F....
(::) failed steps (::)
expected: 5
     got: 0
(compared using ==)
 (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/booking_with_joy_steps.rb:41:in `"visitor is provided with rooms {string}"'
features/booking_a_room.feature:48:in `Then visitor is provided with rooms "2, 3, 4, 5, 6"'
features/booking_a_room.feature:44:in `Then visitor is provided with rooms "<rooms>"'
expected: 4
     got: 0
(compared using ==)
 (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/booking_with_joy_steps.rb:41:in `"visitor is provided with rooms {string}"'
features/booking_a_room.feature:49:in `Then visitor is provided with rooms "3, 4, 5, 6"'
features/booking_a_room.feature:44:in `Then visitor is provided with rooms "<rooms>"'
expected: 3
     got: 0
(compared using ==)
 (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/booking_with_joy_steps.rb:41:in `"visitor is provided with rooms {string}"'
features/booking_a_room.feature:50:in `Then visitor is provided with rooms "4, 5, 6"'
features/booking_a_room.feature:44:in `Then visitor is provided with rooms "<rooms>"'
expected: 2
     got: 0
(compared using ==)
 (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/booking_with_joy_steps.rb:41:in `"visitor is provided with rooms {string}"'
features/booking_a_room.feature:51:in `Then visitor is provided with rooms "5, 6"'
features/booking_a_room.feature:44:in `Then visitor is provided with rooms "<rooms>"'
expected: 1
     got: 0
(compared using ==)
 (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/booking_with_joy_steps.rb:41:in `"visitor is provided with rooms {string}"'
features/booking_a_room.feature:52:in `Then visitor is provided with rooms "6"'
features/booking_a_room.feature:44:in `Then visitor is provided with rooms "<rooms>"'
Failing Scenarios:
cucumber features/booking_a_room.feature:48 # Scenario Outline: Some rooms are not available, Examples (#1)
cucumber features/booking_a_room.feature:49 # Scenario Outline: Some rooms are not available, Examples (#2)
cucumber features/booking_a_room.feature:50 # Scenario Outline: Some rooms are not available, Examples (#3)
cucumber features/booking_a_room.feature:51 # Scenario Outline: Some rooms are not available, Examples (#4)
cucumber features/booking_a_room.feature:52 # Scenario Outline: Some rooms are not available, Examples (#5)
13 scenarios (5 failed, 8 passed)
52 steps (5 failed, 47 passed)
0m0.107s
$
You can see the same error for all the scenarios:
./features/step_definitions/booking_with_joy_steps.rb:41:in `"visitor is provided with rooms {string}"'
This is pointing here:
expect(@available_rooms.size).to eq(expected_room_numbers.size)

So the @available_rooms which is instantiated as the result of @hotel.check_availability(@booking_request) returns an empty array.

Looking at the implementation of this method, you don’t see any error. But with little debugging, you can see that the problem is that, although some rooms are flagged as reserved, the rest of the rooms were not flagged as available. You now know that you should change the design of your Hotel class to render the rooms as available at the beginning. Let’s do it. The following is the new version of the hotel.rb file (Listing 7-31).
# File: hotel.rb
#
class Hotel
  def initialize(rooms:)
    @rooms = rooms
    @rooms.each { |room| room.set_available }
  end
  def check_availability(booking_request)
    existing_available_rooms.select do |existing_available_room|
      existing_available_room.accommodates >= booking_request.guests
    end
  end
  def reserve_room(room_number)
    @rooms.select {|r| r.number == room_number}.first.reserve
  end
  private
  attr_reader :rooms
  def existing_available_rooms
    rooms.select { |room| room.available? }
  end
end
Listing 7-31

New Version of the hotel.rb File That Flags Rooms as Available

You have only added line 6, which renders all the rooms the hotel is initialized with to be available.

If you run cucumber again, you will get this:
$ cucumber --format progress
13 scenarios (13 passed)
52 steps (52 passed)
0m0.067s
$

Excellent! All the Scenarios are now passing successfully!

Increase Your Confidence with Some Extra Examples

The second Scenario Outline has examples only for guests being 1. Let’s add some more examples for other guests numbers (Listing 7-32).
Examples:
  | reserved_rooms   | guests | rooms         |
  | 1                | 1      | 2, 3, 4, 5, 6 |
  | 1, 2             | 1      | 3, 4, 5, 6    |
  | 1, 2, 3          | 1      | 4, 5, 6       |
  | 1, 2, 3, 4       | 1      | 5, 6          |
  | 1, 2, 3, 4, 5    | 1      | 6             |
  | 1, 2, 3, 4, 5, 6 | 1      |               |
  | 1                | 2      | 2, 3, 4, 5, 6 |
  | 1, 2             | 2      | 3, 4, 5, 6    |
  | 1, 2, 3          | 2      | 4, 5, 6       |
  | 1, 2, 3, 4       | 2      | 5, 6          |
  | 1, 2, 3, 4, 5    | 2      | 6             |
  | 1, 2, 3, 4, 5, 6 | 2      |               |
  | 1                | 3      | 5, 6          |
  | 1, 2             | 3      | 5, 6          |
  | 1, 2, 3          | 3      | 5, 6          |
  | 1, 2, 3, 4       | 3      | 5, 6          |
  | 1, 2, 3, 4, 5    | 3      | 6             |
  | 1, 2, 3, 4, 5, 6 | 3      |               |
  | 1                | 4      | 5, 6          |
  | 1, 2             | 4      | 5, 6          |
  | 1, 2, 3          | 4      | 5, 6          |
  | 1, 2, 3, 4       | 4      | 5, 6          |
  | 1, 2, 3, 4, 5    | 4      | 6             |
  | 1, 2, 3, 4, 5, 6 | 4      |               |
  | 1                | 5      |               |
  | 1, 2             | 5      |               |
  | 1, 2, 3          | 5      |               |
  | 1, 2, 3, 4       | 5      |               |
  | 1, 2, 3, 4, 5    | 5      |               |
  | 1, 2, 3, 4, 5, 6 | 5      |               |
  | 1                | 6      |               |
  | 1, 2             | 6      |               |
  | 1, 2, 3          | 6      |               |
  | 1, 2, 3, 4       | 6      |               |
  | 1, 2, 3, 4, 5    | 6      |               |
  | 1, 2, 3, 4, 5, 6 | 6      |               |
Listing 7-32

Second Scenario Outline Examples, New Version

Try running cucumber again, and you will see that all examples are passing successfully:
$ cucumber --format progress
43 scenarios (43 passed)
172 steps (172 passed)
0m0.113s
$

RSpec and Unit Tests

You will continue writing more Scenarios to cover for the various business cases. These will change/enhance your classes’ public interfaces and their implementation. But relying only on the Cucumber Features and Scenarios to build robust classes is not always/usually enough. You will need to write some unit tests that would demonstrate and cover for the classes being used under edge/unexpected cases.

For example, calling Hotel#reserve_room(1000) would raise an exception. Try to execute the following commands in the irb console like I have done in the following:
$ bundle exec irb
irb(main):001:0> require_relative 'all'
=> true
irb(main):002:0> @hotel = Hotel.new(rooms: [Room.new(number: 1, accommodates: 5)])
=> #<Hotel:0x00007fb2c41d9f10 @rooms=[#<Room:0x00007fb2c41d9fb0 @number=1, @accommodates=5, @state=:available>]>
irb(main):003:0> @hotel.reserve_room(1)
=> :reserved
irb(main):004:0> @hotel.reserve_room(1000)
Traceback (most recent call last):
....<traceback here>....
NoMethodError (undefined method `reserve' for nil:NilClass)
irb(main):005:0>

As you can see, the NoMethodError exception is raised when you call @hotel.reserve_room(1000).

It may not be good for your class to behave like this. Instead of raising the NoMethodError exception, you might want to raise a more meaningful error, like ArgumentError with a descriptive message.

How would you go for the development of this, if you were to do TDD? You would have to go with a unit test.

You will write your unit test using RSpec, and before you do, you have to initialize RSpec with the following command. Run it at the root folder of your project:
$ bundle exec rspec –init
  create .rspec
  create spec/spec_helper.rb
$

Then, make sure that the line require_relative '../all' exists at the bottom of the spec/spec_helper.rb file.

Now, you are ready to write your Hotel class unit tests. Create the file spec/hotel_spec.rb like the following (Listing 7-33).
# File spec/hotel_spec.rb
#
describe Hotel do
  describe '#reserve_room' do
    context 'when room number does not exist' do
      let(:maximum_room_number) { rooms.map(&:number).max }
      let(:room_number) { maximum_room_number + 1 }
      let(:rooms) do
        [
          Room.new(number: 1, accommodates: 2),
          Room.new(number: 2, accommodates: 2),
        ]
      end
      subject(:hotel) { Hotel.new(rooms: rooms) }
      it 'raises a meaningful exception' do
        expect do
          hotel.reserve_room room_number
        end.to raise_error(ArgumentError, /room_number: #{room_number} given is not part of the numbers of the rooms of the hotel./)
      end
    end
  end
end
Listing 7-33

Content of the New File spec/hotel_spec.rb

This file contains a unit test for the #reserve_room method that will fail as follows the first time you run it:
$ bundle exec rspec
F
Failures:
  1) Hotel#reserve_room when room number does not exist raises a meaningful exception
     Failure/Error:
       expect do
         hotel.reserve_room room_number
       end.to raise_error(ArgumentError, /room_number: #{room_number} given is not part of the numbers of the rooms of the hotel./)
       expected ArgumentError with message matching /room_number: 3 given is not part of the numbers of the rooms of the hotel./, got #<NoMethodError: undefined method `reserve' for nil:NilClass> with backtrace:
         # ./hotel.rb:16:in `reserve_room'
         # ./spec/hotel_spec.rb:18:in `block (5 levels) in <top (required)>'
         # ./spec/hotel_spec.rb:17:in `block (4 levels) in <top (required)>'
     # ./spec/hotel_spec.rb:17:in `block (4 levels) in <top (required)>'
Finished in 0.03986 seconds (files took 0.16413 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/hotel_spec.rb:16 # Hotel#reserve_room when room number does not exist raises a meaningful exception
$
In order to fix this specification, you need to change the implementation of the Hotel#reserve_room to do what the specification requires. The following is the new content of the file hotel.rb that deals with this (Listing 7-34).
# File: hotel.rb
#
class Hotel
  def initialize(rooms:)
    @rooms = rooms
    @rooms.each { |room| room.set_available }
  end
  def check_availability(booking_request)
    existing_available_rooms.select do |existing_available_room|
      existing_available_room.accommodates >= booking_request.guests
    end
  end
  def reserve_room(room_number)
    room_found = @rooms.select {|r| r.number == room_number}.first
    raise ArgumentError, "room_number: #{room_number} given is not part of the numbers of the rooms of the hotel." if room_found.nil?
    room_found.reserve
  end
  private
  attr_reader :rooms
  def existing_available_rooms
    rooms.select { |room| room.available? }
  end
end
Listing 7-34

Raise ArgumentError to Cover for the Requirement

See lines 15–19. The new implementation of the Hotel#reserve_room makes sure it raises the correct exception with the correct message, according to the requirement.

Let’s run rspec again:
$ bundle exec rspec
.
Finished in 0.00742 seconds (files took 0.22031 seconds to load)
1 example, 0 failures
$

Excellent!

What have you seen? You have seen how parts of your tests are covered with business-facing scenarios using Cucumber and Gherkin and how other parts of your code are tests covered with unit tests using RSpec. With Rspec, you specify implementation details that have a technical focus that would make your class design more robust.

Rest of the Feature

The Feature for booking is missing scenarios that would cover the cases to take into account the dates rooms are available. I leave this as an exercise to you.

Cucumber Command Line

Before I close the chapter on Cucumber, I am going to present to you some aspects of the cucumber tool itself. Then I will show you how RubyMine integrates with Cucumber.

Dry Run

cucumber allows you to execute a dry run. The dry run parses your .feature files and makes sure that they are ok with regard to their syntax. Basically, the dry run skips all the steps. Also, it reports any step definitions that are not defined.

Try, for example, the dry run for the booking-with-joy project:
$ cucumber --dry-run
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:                        # features/booking_a_room.feature:8
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel # features/step_definitions/booking_with_joy_steps.rb:5
      | number | accommodates |
      | 1      | 2            |
      | 2      | 2            |
      | 3      | 2            |
      | 4      | 2            |
      | 5      | 4            |
      | 6      | 4            |
  Scenario Outline: All the rooms are free and there are rooms on the capacity requested # features/booking_a_room.feature:22
    Given all the rooms are available      # features/booking_a_room.feature:23
    When visitor provides the following booking details        # features/booking_a_room.feature:24
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"      # features/booking_a_room.feature:27
    Examples:
      | guests | rooms            |
      | 1      | 1, 2, 3, 4, 5, 6 |
      | 2      | 1, 2, 3, 4, 5, 6 |
      | 3      | 5, 6             |
      | 4      | 5, 6             |
      | 5      |                  |
      | 6      |                  |
      | 7      |                  |
  Scenario Outline: Some rooms are not available        # features/booking_a_room.feature:39
    Given the rooms "<reserved_rooms>" are reserved     # features/booking_a_room.feature:40
    When visitor provides the following booking details # features/booking_a_room.feature:41
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"       # features/booking_a_room.feature:44
    Examples:
      | reserved_rooms   | guests | rooms         |
      | 1                | 1      | 2, 3, 4, 5, 6 |
      | 1, 2             | 1      | 3, 4, 5, 6    |
      | 1, 2, 3          | 1      | 4, 5, 6       |
      | 1, 2, 3, 4       | 1      | 5, 6          |
      | 1, 2, 3, 4, 5    | 1      | 6             |
      | 1, 2, 3, 4, 5, 6 | 1      |               |
      | 1                | 2      | 2, 3, 4, 5, 6 |
      | 1, 2             | 2      | 3, 4, 5, 6    |
      | 1, 2, 3          | 2      | 4, 5, 6       |
      | 1, 2, 3, 4       | 2      | 5, 6          |
      | 1, 2, 3, 4, 5    | 2      | 6             |
      | 1, 2, 3, 4, 5, 6 | 2      |               |
      | 1                | 3      | 5, 6          |
      | 1, 2             | 3      | 5, 6          |
      | 1, 2, 3          | 3      | 5, 6          |
      | 1, 2, 3, 4       | 3      | 5, 6          |
      | 1, 2, 3, 4, 5    | 3      | 6             |
      | 1, 2, 3, 4, 5, 6 | 3      |               |
      | 1                | 4      | 5, 6          |
      | 1, 2             | 4      | 5, 6          |
      | 1, 2, 3          | 4      | 5, 6          |
      | 1, 2, 3, 4       | 4      | 5, 6          |
      | 1, 2, 3, 4, 5    | 4      | 6             |
      | 1, 2, 3, 4, 5, 6 | 4      |               |
      | 1                | 5      |               |
      | 1, 2             | 5      |               |
      | 1, 2, 3          | 5      |               |
      | 1, 2, 3, 4       | 5      |               |
      | 1, 2, 3, 4, 5    | 5      |               |
      | 1, 2, 3, 4, 5, 6 | 5      |               |
      | 1                | 6      |               |
      | 1, 2             | 6      |               |
      | 1, 2, 3          | 6      |               |
      | 1, 2, 3, 4       | 6      |               |
      | 1, 2, 3, 4, 5    | 6      |               |
      | 1, 2, 3, 4, 5, 6 | 6      |               |
43 scenarios (43 skipped)
172 steps (172 skipped)

Invoking a Single Feature

Usually, your project is going to have multiple .feature files . Calling cucumber will execute all the Scenarios defined in all these files. How can you invoke cucumber for the Scenarios of a particular .feature file only?

This is how you should do it: In the following example, I invoke the Scenarios inside the file features/booking_online.feature. Other .feature files are ignored:
$ cucumber features/booking_online.feature

Invoking a Single Scenario

Besides being able to specify a specific .feature file, you are able to specify a specific Scenario within a specific .feature file.

Let’s consider again the booking-with-joy project and the .feature file:
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:
    The table below describes the rooms available. The "number"
    column is the room number. The "accommodates" column is the
    maximum number of people that can stay in the room
    Given there are the following rooms in the hotel
      | number | accommodates |
      |      1 |            2 |
      |      2 |            2 |
      |      3 |            2 |
      |      4 |            2 |
      |      5 |            4 |
      |      6 |            4 |
  Scenario Outline: All the rooms are free and there are rooms on the capacity requested
    Given all the rooms are available
    When visitor provides the following booking details
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"
    Examples:
      | guests | rooms            |
      |      1 | 1, 2, 3, 4, 5, 6 |
      |      2 | 1, 2, 3, 4, 5, 6 |
      |      3 |             5, 6 |
      |      4 |             5, 6 |
      |      5 |                  |
      |      6 |                  |
      |      7 |                  |
  Scenario Outline: Some rooms are not available
    Given the rooms "<reserved_rooms>" are reserved
    When visitor provides the following booking details
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"
    Examples:
      | reserved_rooms   | guests | rooms         |
      | 1                | 1      | 2, 3, 4, 5, 6 |
      | 1, 2             | 1      | 3, 4, 5, 6    |
      | 1, 2, 3          | 1      | 4, 5, 6       |
      | 1, 2, 3, 4       | 1      | 5, 6          |
      | 1, 2, 3, 4, 5    | 1      | 6             |
      | 1, 2, 3, 4, 5, 6 | 1      |               |
      | 1                | 2      | 2, 3, 4, 5, 6 |
      | 1, 2             | 2      | 3, 4, 5, 6    |
      | 1, 2, 3          | 2      | 4, 5, 6       |
      | 1, 2, 3, 4       | 2      | 5, 6          |
      | 1, 2, 3, 4, 5    | 2      | 6             |
      | 1, 2, 3, 4, 5, 6 | 2      |               |
      | 1                | 3      | 5, 6          |
      | 1, 2             | 3      | 5, 6          |
      | 1, 2, 3          | 3      | 5, 6          |
      | 1, 2, 3, 4       | 3      | 5, 6          |
      | 1, 2, 3, 4, 5    | 3      | 6             |
      | 1, 2, 3, 4, 5, 6 | 3      |               |
      | 1                | 4      | 5, 6          |
      | 1, 2             | 4      | 5, 6          |
      | 1, 2, 3          | 4      | 5, 6          |
      | 1, 2, 3, 4       | 4      | 5, 6          |
      | 1, 2, 3, 4, 5    | 4      | 6             |
      | 1, 2, 3, 4, 5, 6 | 4      |               |
      | 1                | 5      |               |
      | 1, 2             | 5      |               |
      | 1, 2, 3          | 5      |               |
      | 1, 2, 3, 4       | 5      |               |
      | 1, 2, 3, 4, 5    | 5      |               |
      | 1, 2, 3, 4, 5, 6 | 5      |               |
      | 1                | 6      |               |
      | 1, 2             | 6      |               |
      | 1, 2, 3          | 6      |               |
      | 1, 2, 3, 4       | 6      |               |
      | 1, 2, 3, 4, 5    | 6      |               |
      | 1, 2, 3, 4, 5, 6 | 6      |               |
You can execute the Scenarios corresponding to the Scenario Outline defined on line 22 as follows:
$ cucumber features/booking_a_room.feature:22
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:                          # features/booking_a_room.feature:8
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel # features/step_definitions/booking_with_joy_steps.rb:5
      | number | accommodates |
      | 1      | 2            |
      | 2      | 2            |
      | 3      | 2            |
      | 4      | 2            |
      | 5      | 4            |
      | 6      | 4            |
  Scenario Outline: All the rooms are free and there are rooms on the capacity requested # features/booking_a_room.feature:22
    Given all the rooms are available                 # features/booking_a_room.feature:23
    When visitor provides the following booking details       # features/booking_a_room.feature:24
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"            # features/booking_a_room.feature:27
    Examples:
      | guests | rooms            |
      | 1      | 1, 2, 3, 4, 5, 6 |
      | 2      | 1, 2, 3, 4, 5, 6 |
      | 3      | 5, 6             |
      | 4      | 5, 6             |
      | 5      |                  |
      | 6      |                  |
      | 7      |                  |
7 scenarios (7 passed)
28 steps (28 passed)
0m0.208s
$

As you can see, I have managed to call the Scenarios that correspond to the Scenario Outline on line 22, by specifying the line number next to the filename of the feature: cucumber features/booking_a_room.feature:22.

But you can take it even further. You can ask cucumber to execute the Scenario that corresponds to a specific example. Here is how you can execute the Scenario that corresponds to the example on line 34:
$ cucumber features/booking_a_room.feature:34
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:                         # features/booking_a_room.feature:8
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel # features/step_definitions/booking_with_joy_steps.rb:5
      | number | accommodates |
      | 1      | 2            |
      | 2      | 2            |
      | 3      | 2            |
      | 4      | 2            |
      | 5      | 4            |
      | 6      | 4            |
  Scenario Outline: All the rooms are free and there are rooms on the capacity requested # features/booking_a_room.feature:22
    Given all the rooms are available                 # features/booking_a_room.feature:23
    When visitor provides the following booking details           # features/booking_a_room.feature:24
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"            # features/booking_a_room.feature:27
    Examples:
      | guests | rooms            |
      | 4      | 5, 6             |
1 scenario (1 passed)
4 steps (4 passed)
0m0.075s
$

So, instead of specifying the line of the Scenario Outline, you specified the line of the Example.

Note that you can ask cucumber to execute more than one scenario at the same time. For example, in the following, I execute the scenarios that correspond to the examples for lines 34 and 35. See how I give the list of line numbers in a :-separated list:
$ cucumber features/booking_a_room.feature:34:35
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:                  # features/booking_a_room.feature:8
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel # features/step_definitions/booking_with_joy_steps.rb:5
      | number | accommodates |
      | 1      | 2            |
      | 2      | 2            |
      | 3      | 2            |
      | 4      | 2            |
      | 5      | 4            |
      | 6      | 4            |
  Scenario Outline: All the rooms are free and there are rooms on the capacity requested # features/booking_a_room.feature:22
    Given all the rooms are available         # features/booking_a_room.feature:23
    When visitor provides the following booking details          # features/booking_a_room.feature:24
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"              # features/booking_a_room.feature:27
    Examples:
      | guests | rooms            |
      | 4      | 5, 6             |
      | 5      |                  |
2 scenarios (2 passed)
8 steps (8 passed)
0m0.070s
$

Tags

Cucumber allows you to tag your Features and your Scenarios (including Scenario Outlines) with tags. Then, you can tell cucumber which tags you are interested in, and cucumber will only execute the Scenarios tagged with these tags .

For example, let’s tag one of the two Scenario Outlines with the tag @foo (Listing 7-35).
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel
      | number | accommodates |
      |      1 |            2 |
      |      2 |            2 |
      |      3 |            2 |
      |      4 |            2 |
      |      5 |            4 |
      |      6 |            4 |
  Scenario Outline: All the rooms are free and there are rooms on the capacity requested
    Given all the rooms are available
    When visitor provides the following booking details
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"
    Examples:
      | guests | rooms            |
      |      1 | 1, 2, 3, 4, 5, 6 |
      |      2 | 1, 2, 3, 4, 5, 6 |
      |      3 |             5, 6 |
      |      4 |             5, 6 |
      |      5 |                  |
      |      6 |                  |
      |      7 |                  |
  @foo
  Scenario Outline: Some rooms are not available
    Given the rooms "<reserved_rooms>" are reserved
    When visitor provides the following booking details
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"
    Examples:
      | reserved_rooms   | guests | rooms         |
      | 1                | 1      | 2, 3, 4, 5, 6 |
      | 1, 2             | 1      | 3, 4, 5, 6    |
      | 1, 2, 3          | 1      | 4, 5, 6       |
      | 1, 2, 3, 4       | 1      | 5, 6          |
      | 1, 2, 3, 4, 5    | 1      | 6             |
      | 1, 2, 3, 4, 5, 6 | 1      |               |
      | 1                | 2      | 2, 3, 4, 5, 6 |
      | 1, 2             | 2      | 3, 4, 5, 6    |
      | 1, 2, 3          | 2      | 4, 5, 6       |
      | 1, 2, 3, 4       | 2      | 5, 6          |
      | 1, 2, 3, 4, 5    | 2      | 6             |
      | 1, 2, 3, 4, 5, 6 | 2      |               |
      | 1                | 3      | 5, 6          |
      | 1, 2             | 3      | 5, 6          |
      | 1, 2, 3          | 3      | 5, 6          |
      | 1, 2, 3, 4       | 3      | 5, 6          |
      | 1, 2, 3, 4, 5    | 3      | 6             |
      | 1, 2, 3, 4, 5, 6 | 3      |               |
      | 1                | 4      | 5, 6          |
      | 1, 2             | 4      | 5, 6          |
      | 1, 2, 3          | 4      | 5, 6          |
      | 1, 2, 3, 4       | 4      | 5, 6          |
      | 1, 2, 3, 4, 5    | 4      | 6             |
      | 1, 2, 3, 4, 5, 6 | 4      |               |
      | 1                | 5      |               |
      | 1, 2             | 5      |               |
      | 1, 2, 3          | 5      |               |
      | 1, 2, 3, 4       | 5      |               |
      | 1, 2, 3, 4, 5    | 5      |               |
      | 1, 2, 3, 4, 5, 6 | 5      |               |
      | 1                | 6      |               |
      | 1, 2             | 6      |               |
      | 1, 2, 3          | 6      |               |
      | 1, 2, 3, 4       | 6      |               |
      | 1, 2, 3, 4, 5    | 6      |               |
      | 1, 2, 3, 4, 5, 6 | 6      |               |
Listing 7-35

Using Tags

Do you see line 39? This is the only change. I have tagged the Scenario Outline with the tag @foo. Now, let’s invoke cucumber and tell it to execute the Scenarios that are tagged with this tag only:
$ cucumber --tags @foo
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:                 # features/booking_a_room.feature:8
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel # features/step_definitions/booking_with_joy_steps.rb:5
      | number | accommodates |
      | 1      | 2            |
      | 2      | 2            |
      | 3      | 2            |
      | 4      | 2            |
      | 5      | 4            |
      | 6      | 4            |
  @foo
  Scenario Outline: Some rooms are not available        # features/booking_a_room.feature:40
    Given the rooms "<reserved_rooms>" are reserved     # features/booking_a_room.feature:41
    When visitor provides the following booking details # features/booking_a_room.feature:42
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"       # features/booking_a_room.feature:45
    Examples:
      | reserved_rooms   | guests | rooms         |
      | 1                | 1      | 2, 3, 4, 5, 6 |
      | 1, 2             | 1      | 3, 4, 5, 6    |
      | 1, 2, 3          | 1      | 4, 5, 6       |
      | 1, 2, 3, 4       | 1      | 5, 6          |
      | 1, 2, 3, 4, 5    | 1      | 6             |
      | 1, 2, 3, 4, 5, 6 | 1      |               |
      | 1                | 2      | 2, 3, 4, 5, 6 |
      | 1, 2             | 2      | 3, 4, 5, 6    |
      | 1, 2, 3          | 2      | 4, 5, 6       |
      | 1, 2, 3, 4       | 2      | 5, 6          |
      | 1, 2, 3, 4, 5    | 2      | 6             |
      | 1, 2, 3, 4, 5, 6 | 2      |               |
      | 1                | 3      | 5, 6          |
      | 1, 2             | 3      | 5, 6          |
      | 1, 2, 3          | 3      | 5, 6          |
      | 1, 2, 3, 4       | 3      | 5, 6          |
      | 1, 2, 3, 4, 5    | 3      | 6             |
      | 1, 2, 3, 4, 5, 6 | 3      |               |
      | 1                | 4      | 5, 6          |
      | 1, 2             | 4      | 5, 6          |
      | 1, 2, 3          | 4      | 5, 6          |
      | 1, 2, 3, 4       | 4      | 5, 6          |
      | 1, 2, 3, 4, 5    | 4      | 6             |
      | 1, 2, 3, 4, 5, 6 | 4      |               |
      | 1                | 5      |               |
      | 1, 2             | 5      |               |
      | 1, 2, 3          | 5      |               |
      | 1, 2, 3, 4       | 5      |               |
      | 1, 2, 3, 4, 5    | 5      |               |
      | 1, 2, 3, 4, 5, 6 | 5      |               |
      | 1                | 6      |               |
      | 1, 2             | 6      |               |
      | 1, 2, 3          | 6      |               |
      | 1, 2, 3, 4       | 6      |               |
      | 1, 2, 3, 4, 5    | 6      |               |
      | 1, 2, 3, 4, 5, 6 | 6      |               |
36 scenarios (36 passed)
144 steps (144 passed)
0m0.246s
$

I invoked cucumber as cucumber --tags @foo, and this executed only the Scenarios that were tagged with the @foo tag. You can ask for more than one tag to be executed – cucumber --tags @tag1,@tag2,@tag3 – in a comma-separated list after the --tags switch.

But tags can also be useful when you want to exclude some Scenarios from being executed. For example, you may not want some Scenarios to be executed in your continuous integration server.

How can you tell that Scenarios of a specific tag should be excluded? This is done by pre-pending the name of the tag with the ~ symbol.

The following invokes cucumber on the booking-with-joy project, without executing the @foo-tagged Scenarios:
$ cucumber --tags ~@foo
Deprecated: Found tags option '~@foo'. Support for '~@tag' will be removed from the next release of Cucumber. Please use 'not @tag' instead.
# File: features/booking_a_room.feature
#
Feature: Booking a Room
  Users should be able to book a room.
  The application should ask user the booking details and
  then return back the rooms available.
  Background:                     # features/booking_a_room.feature:8
  The table below describes the rooms available. The "number"
  column is the room number. The "accommodates" column is the
  maximum number of people that can stay in the room
    Given there are the following rooms in the hotel # features/step_definitions/booking_with_joy_steps.rb:5
      | number | accommodates |
      | 1      | 2            |
      | 2      | 2            |
      | 3      | 2            |
      | 4      | 2            |
      | 5      | 4            |
      | 6      | 4            |
  Scenario Outline: All the rooms are free and there are rooms on the capacity requested # features/booking_a_room.feature:22
    Given all the rooms are available          # features/booking_a_room.feature:23
    When visitor provides the following booking details       # features/booking_a_room.feature:24
      | check_in    | check_out   | guests   |
      | 23-Mar-2017 | 25-Mar-2017 | <guests> |
    Then visitor is provided with rooms "<rooms>"       # features/booking_a_room.feature:27
    Examples:
      | guests | rooms            |
      | 1      | 1, 2, 3, 4, 5, 6 |
      | 2      | 1, 2, 3, 4, 5, 6 |
      | 3      | 5, 6             |
      | 4      | 5, 6             |
      | 5      |                  |
      | 6      |                  |
      | 7      |                  |
7 scenarios (7 passed)
28 steps (28 passed)
0m0.107s
$

Hooks

Cucumber allows you to hook a piece of Ruby code that will be executed at specific points in the lifecycle of the execution of your Scenarios, in particular
  1. 1.

    Before every Scenario

     
  2. 2.

    After every Scenario

     
  3. 3.

    Around every Scenario

     
  4. 4.

    After every Step

     
The hooks are usually defined inside the file features/support/hooks.rb. Let’s put some dummy implementation for our booking-with-joy project inside this file (Listing 7-36).
# File: features/support/hooks.rb
#
Before do |scenario|
  print "Before starting scenario #{scenario.name}"
end
After do |scenario|
  puts "> after scenario #{scenario.name}"
end
Listing 7-36

Dummy Hooks Implementation

In the preceding features/support/hooks.rb file, you hook some Ruby code that would be executed both before and after each Scenario. Note that you do have access to the metadata of the Scenario that is executed via the block-level variable (scenario).

If you run the cucumber command, you will get this:
$ cucumber --format progress
Before starting scenario All the rooms are free and there are rooms on the capacity requested, Examples (#1)....
> after scenario All the rooms are free and there are rooms on the capacity requested, Examples (#1)
Before starting scenario All the rooms are free and there are rooms on the capacity requested, Examples (#2)....
> after scenario All the rooms are free and there are rooms on the capacity requested, Examples (#2)
Before starting scenario All the rooms are free and there are rooms on the capacity requested, Examples (#3)....
> after scenario All the rooms are free and there are rooms on the capacity requested, Examples (#3)
Before starting scenario All the rooms are free and there are rooms on the capacity requested, Examples (#4)....
> after scenario All the rooms are free and there are rooms on the capacity requested, Examples (#4)
Before starting scenario All the rooms are free and there are rooms on the capacity requested, Examples (#5)....
> after scenario All the rooms are free and there are rooms on the capacity requested, Examples (#5)
Before starting scenario All the rooms are free and there are rooms on the capacity requested, Examples (#6)....
> after scenario All the rooms are free and there are rooms on the capacity requested, Examples (#6)
Before starting scenario All the rooms are free and there are rooms on the capacity requested, Examples (#7)....
> after scenario All the rooms are free and there are rooms on the capacity requested, Examples (#7)
Before starting scenario Some rooms are not available, Examples (#1)....
> after scenario Some rooms are not available, Examples (#1)
Before starting scenario Some rooms are not available, Examples (#2)....
> after scenario Some rooms are not available, Examples (#2)
Before starting scenario Some rooms are not available, Examples (#3)....
> after scenario Some rooms are not available, Examples (#3)
Before starting scenario Some rooms are not available, Examples (#4)....
> after scenario Some rooms are not available, Examples (#4)
Before starting scenario Some rooms are not available, Examples (#5)....
> after scenario Some rooms are not available, Examples (#5)
Before starting scenario Some rooms are not available, Examples (#6)....
> after scenario Some rooms are not available, Examples (#6)
Before starting scenario Some rooms are not available, Examples (#7)....
> after scenario Some rooms are not available, Examples (#7)
Before starting scenario Some rooms are not available, Examples (#8)....
> after scenario Some rooms are not available, Examples (#8)
Before starting scenario Some rooms are not available, Examples (#9)....
> after scenario Some rooms are not available, Examples (#9)
Before starting scenario Some rooms are not available, Examples (#10)....
> after scenario Some rooms are not available, Examples (#10)
Before starting scenario Some rooms are not available, Examples (#11)....
> after scenario Some rooms are not available, Examples (#11)
Before starting scenario Some rooms are not available, Examples (#12)....
> after scenario Some rooms are not available, Examples (#12)
Before starting scenario Some rooms are not available, Examples (#13)....
> after scenario Some rooms are not available, Examples (#13)
Before starting scenario Some rooms are not available, Examples (#14)....
> after scenario Some rooms are not available, Examples (#14)
Before starting scenario Some rooms are not available, Examples (#15)....
> after scenario Some rooms are not available, Examples (#15)
Before starting scenario Some rooms are not available, Examples (#16)....
> after scenario Some rooms are not available, Examples (#16)
Before starting scenario Some rooms are not available, Examples (#17)....
> after scenario Some rooms are not available, Examples (#17)
Before starting scenario Some rooms are not available, Examples (#18)....
> after scenario Some rooms are not available, Examples (#18)
Before starting scenario Some rooms are not available, Examples (#19)....
> after scenario Some rooms are not available, Examples (#19)
Before starting scenario Some rooms are not available, Examples (#20)....
> after scenario Some rooms are not available, Examples (#20)
Before starting scenario Some rooms are not available, Examples (#21)....
> after scenario Some rooms are not available, Examples (#21)
Before starting scenario Some rooms are not available, Examples (#22)....
> after scenario Some rooms are not available, Examples (#22)
Before starting scenario Some rooms are not available, Examples (#23)....
> after scenario Some rooms are not available, Examples (#23)
Before starting scenario Some rooms are not available, Examples (#24)....
> after scenario Some rooms are not available, Examples (#24)
Before starting scenario Some rooms are not available, Examples (#25)....
> after scenario Some rooms are not available, Examples (#25)
Before starting scenario Some rooms are not available, Examples (#26)....
> after scenario Some rooms are not available, Examples (#26)
Before starting scenario Some rooms are not available, Examples (#27)....
> after scenario Some rooms are not available, Examples (#27)
Before starting scenario Some rooms are not available, Examples (#28)....
> after scenario Some rooms are not available, Examples (#28)
Before starting scenario Some rooms are not available, Examples (#29)....
> after scenario Some rooms are not available, Examples (#29)
Before starting scenario Some rooms are not available, Examples (#30)....
> after scenario Some rooms are not available, Examples (#30)
Before starting scenario Some rooms are not available, Examples (#31)....
> after scenario Some rooms are not available, Examples (#31)
Before starting scenario Some rooms are not available, Examples (#32)....
> after scenario Some rooms are not available, Examples (#32)
Before starting scenario Some rooms are not available, Examples (#33)....
> after scenario Some rooms are not available, Examples (#33)
Before starting scenario Some rooms are not available, Examples (#34)....
> after scenario Some rooms are not available, Examples (#34)
Before starting scenario Some rooms are not available, Examples (#35)....
> after scenario Some rooms are not available, Examples (#35)
Before starting scenario Some rooms are not available, Examples (#36)....
> after scenario Some rooms are not available, Examples (#36)
43 scenarios (43 passed)
172 steps (172 passed)
0m0.154s
$

You can see how the Ruby code hooked to Before and After hooks is being invoked and its output is being printed in between the standard cucumber output.

The hooks may be scoped for specific tags. Let’s change the features/support/hooks.rb file to hook to the @foo-tagged Scenarios only (Listing 7-37).
# File: features/support/hooks.rb
#
Before('@foo') do |scenario|
  print "Before starting scenario #{scenario.name}"
end
After('@foo') do |scenario|
  puts "> after scenario #{scenario.name}"
end
Listing 7-37

Hooks for Specific Tags

You can see how I scope the hooks, by giving the tag I want to scope for as argument to the hook methods. If you run cucumber again, you will see that the Ruby code output is only printed for the Scenarios that are tagged with the @foo tag:
$ cucumber --format progress
............................Before starting scenario Some rooms are not available, Examples (#1)....
> after scenario Some rooms are not available, Examples (#1)
Before starting scenario Some rooms are not available, Examples (#2)....
> after scenario Some rooms are not available, Examples (#2)
Before starting scenario Some rooms are not available, Examples (#3)....
> after scenario Some rooms are not available, Examples (#3)
Before starting scenario Some rooms are not available, Examples (#4)....
> after scenario Some rooms are not available, Examples (#4)
Before starting scenario Some rooms are not available, Examples (#5)....
> after scenario Some rooms are not available, Examples (#5)
Before starting scenario Some rooms are not available, Examples (#6)....
> after scenario Some rooms are not available, Examples (#6)
Before starting scenario Some rooms are not available, Examples (#7)....
> after scenario Some rooms are not available, Examples (#7)
Before starting scenario Some rooms are not available, Examples (#8)....
> after scenario Some rooms are not available, Examples (#8)
Before starting scenario Some rooms are not available, Examples (#9)....
> after scenario Some rooms are not available, Examples (#9)
Before starting scenario Some rooms are not available, Examples (#10)....
> after scenario Some rooms are not available, Examples (#10)
Before starting scenario Some rooms are not available, Examples (#11)....
> after scenario Some rooms are not available, Examples (#11)
Before starting scenario Some rooms are not available, Examples (#12)....
> after scenario Some rooms are not available, Examples (#12)
Before starting scenario Some rooms are not available, Examples (#13)....
> after scenario Some rooms are not available, Examples (#13)
Before starting scenario Some rooms are not available, Examples (#14)....
> after scenario Some rooms are not available, Examples (#14)
Before starting scenario Some rooms are not available, Examples (#15)....
> after scenario Some rooms are not available, Examples (#15)
Before starting scenario Some rooms are not available, Examples (#16)....
> after scenario Some rooms are not available, Examples (#16)
Before starting scenario Some rooms are not available, Examples (#17)....
> after scenario Some rooms are not available, Examples (#17)
Before starting scenario Some rooms are not available, Examples (#18)....
> after scenario Some rooms are not available, Examples (#18)
Before starting scenario Some rooms are not available, Examples (#19)....
> after scenario Some rooms are not available, Examples (#19)
Before starting scenario Some rooms are not available, Examples (#20)....
> after scenario Some rooms are not available, Examples (#20)
Before starting scenario Some rooms are not available, Examples (#21)....
> after scenario Some rooms are not available, Examples (#21)
Before starting scenario Some rooms are not available, Examples (#22)....
> after scenario Some rooms are not available, Examples (#22)
Before starting scenario Some rooms are not available, Examples (#23)....
> after scenario Some rooms are not available, Examples (#23)
Before starting scenario Some rooms are not available, Examples (#24)....
> after scenario Some rooms are not available, Examples (#24)
Before starting scenario Some rooms are not available, Examples (#25)....
> after scenario Some rooms are not available, Examples (#25)
Before starting scenario Some rooms are not available, Examples (#26)....
> after scenario Some rooms are not available, Examples (#26)
Before starting scenario Some rooms are not available, Examples (#27)....
> after scenario Some rooms are not available, Examples (#27)
Before starting scenario Some rooms are not available, Examples (#28)....
> after scenario Some rooms are not available, Examples (#28)
Before starting scenario Some rooms are not available, Examples (#29)....
> after scenario Some rooms are not available, Examples (#29)
Before starting scenario Some rooms are not available, Examples (#30)....
> after scenario Some rooms are not available, Examples (#30)
Before starting scenario Some rooms are not available, Examples (#31)....
> after scenario Some rooms are not available, Examples (#31)
Before starting scenario Some rooms are not available, Examples (#32)....
> after scenario Some rooms are not available, Examples (#32)
Before starting scenario Some rooms are not available, Examples (#33)....
> after scenario Some rooms are not available, Examples (#33)
Before starting scenario Some rooms are not available, Examples (#34)....
> after scenario Some rooms are not available, Examples (#34)
Before starting scenario Some rooms are not available, Examples (#35)....
> after scenario Some rooms are not available, Examples (#35)
Before starting scenario Some rooms are not available, Examples (#36)....
> after scenario Some rooms are not available, Examples (#36)
43 scenarios (43 passed)
172 steps (172 passed)
0m0.109s
$
The Around hook is a useful hook too. For example, you can replace the two Before and After hooks with the following Around hook (Listing 7-38).
# File: features/support/hooks.rb
#
Around('@foo') do |scenario, block|
  print "Before starting scenario #{scenario.name}"
  block.call
  puts "> after scenario #{scenario.name}"
end
Listing 7-38

Using the Around Hook

The point with the Around hook is that its block takes two arguments: the scenario and the block that you can #call on and execute the Scenario itself. If you don’t call block.call, the Scenario is never actually called, and you get an error. Otherwise, whatever code you put before the block.call statement is executed before the Scenario, and whatever you put after the block.call statement is executed after the Scenario at hand.

Besides the Scenario-level hooks, sometimes you might want to take advantage of the hook that is called after a step has finished. This is the AfterStep hook.

Let’s add the AfterStep hook for the @foo Scenarios (Listing 7-39).
# File: features/support/hooks.rb
#
Around('@foo') do |scenario, block|
  print "Before starting scenario #{scenario.name}"
  block.call
  puts "> after scenario #{scenario.name}"
end
AfterStep do |result, step_info|
  puts "Result OK?: #{result.ok?} - Step: #{step_info.text}"
end
Listing 7-39

Using the AfterStep Hook

As you can see, the AfterStep hook yields two arguments to the called block. The result holds information about the result of the execution. The step_info is about which step has just finished execution.

Let’s run cucumber for the Scenario at line 22:
$ cucumber features/booking_a_room.feature:22 --format progress
.
Result OK?: true - Step: there are the following rooms in the hotel
.
Result OK?: true - Step: all the rooms are available
.
Result OK?: true - Step: visitor provides the following booking details
.
Result OK?: true - Step: visitor is provided with rooms "1, 2, 3, 4, 5, 6"
.
Result OK?: true - Step: there are the following rooms in the hotel
.
Result OK?: true - Step: all the rooms are available
.
Result OK?: true - Step: visitor provides the following booking details
.
Result OK?: true - Step: visitor is provided with rooms "1, 2, 3, 4, 5, 6"
.
Result OK?: true - Step: there are the following rooms in the hotel
.
Result OK?: true - Step: all the rooms are available
.
Result OK?: true - Step: visitor provides the following booking details
.
Result OK?: true - Step: visitor is provided with rooms "5, 6"
.
Result OK?: true - Step: there are the following rooms in the hotel
.
Result OK?: true - Step: all the rooms are available
.
Result OK?: true - Step: visitor provides the following booking details
.
Result OK?: true - Step: visitor is provided with rooms "5, 6"
.
Result OK?: true - Step: there are the following rooms in the hotel
.
Result OK?: true - Step: all the rooms are available
.
Result OK?: true - Step: visitor provides the following booking details
.
Result OK?: true - Step: visitor is provided with rooms ""
.
Result OK?: true - Step: there are the following rooms in the hotel
.
Result OK?: true - Step: all the rooms are available
.
Result OK?: true - Step: visitor provides the following booking details
.
Result OK?: true - Step: visitor is provided with rooms ""
.
Result OK?: true - Step: there are the following rooms in the hotel
.
Result OK?: true - Step: all the rooms are available
.
Result OK?: true - Step: visitor provides the following booking details
.
Result OK?: true - Step: visitor is provided with rooms ""
7 scenarios (7 passed)
28 steps (28 passed)
0m0.070s
$

Do you see the lines that are printed after the execution of each step?

Doc Strings

Doc strings or document strings are long strings that you want to use in step invocations. These strings are usually multiline strings that cannot fit in one line. Let’s see, for example, the following Scenario:
Scenario: Multiline input example
  Given the user provides a string such as
    """
    This is a two line string. First Line.
    Second Line.
    """
  When we invoke the encryption of this string
  Then we get a result such as this
    """
    Uijt!jt!b!uxp!mjof!tusjoh/!Gjstu!Mjof/
    Tfdpoe!Mjof/
    """

The doc string is provided in between two lines, each of which has triple quotes """. You usually write the triple quotes indented by two spaces to the right relative to the beginning column of the step the doc string belongs to. Then each line of the doc string needs to start at that column too.

Cucumber and RubyMine Integration

RubyMine has seamless integration for Cucumber:
  1. 1.
    When you invoke a new step that is not defined, it underlines the step (Figure 7-3).
    ../images/497830_1_En_7_Chapter/497830_1_En_7_Fig3_HTML.jpg
    Figure 7-3

    How RubyMine Underlines Undefined Steps

     
  2. 2.

    When you Cmd+Click a step invocation, it takes you directly to the step definition.

     
  3. 3.

    You can right-click a Scenario, and you can invoke the execution of the specific Scenario.

     
  4. 4.

    You can right-click a Feature file, and you can invoke the execution of the particular Feature file.

     

Task Details

Ruby Application With Cucumber and RSpec

You need to implement the following Ruby application covered with both Cucumber Scenarios and RSpec unit tests:

You are selling products, and you would like your application to cover for the following scenarios in the context of generating an invoice:
  1. 1.

    If the total price of the products sold in an order is above 50, then a discount of 10% should be applied.

     
  2. 2.

    When a product is tagged with the tag PROMO25, then a 25% discount should be applied to the product price.

     
  3. 3.

    All the products have a VAT of 20%. Note that the VAT is applied after any product or order discount is applied.

     

You will need to write a Feature file containing various Scenarios that would document the proper order total amount. You will also have to implement the order total calculating logic.

Key Takeaways

Cucumber is a fantastic tool to minimize the impedance mismatch between business stakeholders and developers. And it is available for many programming languages, not only for Ruby. It is used by many companies that want to have living documentation of the features of their applications.

The more you read and practice, the better you become on how to apply Cucumber.

Here is the list of the most important things you learned in this chapter:
  • Important Gherkin keywords, like Feature, Scenario, Scenario Outline, and Examples

  • Three different scenario phases

  • Scenarios states

  • Data tables

  • Feature and Scenario internationalization

  • Using tags

  • Using hooks

This is the last chapter of this book, your journey to practical test automation. I hope that you have learned something from it, namely, practical tips that I use on a daily basis when I develop a project. Hopefully, they will be useful to you too. They will help you become a better developer with increased quality of your software deliverables.

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

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