CrackedRuby logo

CrackedRuby

Cucumber

Behavior-driven development framework for writing executable specifications in Ruby using Gherkin syntax.

Testing and Quality Testing Frameworks
8.1.4

Behavior-driven development framework for writing executable specifications in Ruby using Gherkin syntax.

Overview

Cucumber transforms plain-language feature descriptions into executable tests through step definitions written in Ruby. The framework parses Gherkin syntax files containing scenarios written in Given-When-Then format, matching each step to corresponding Ruby code that interacts with your application.

The core workflow involves creating .feature files with business-readable scenarios, then implementing step definitions that translate natural language into Ruby code. Cucumber executes these scenarios as automated tests, providing feedback on which steps pass or fail.

# features/user_login.feature
Feature: User Authentication
  Scenario: Successful login
    Given a user exists with email "user@example.com"
    When I log in with valid credentials
    Then I should see the dashboard
# features/step_definitions/login_steps.rb
Given('a user exists with email {string}') do |email|
  @user = User.create!(email: email, password: 'password123')
end

When('I log in with valid credentials') do
  visit '/login'
  fill_in 'Email', with: @user.email
  fill_in 'Password', with: 'password123'
  click_button 'Log In'
end

Then('I should see the dashboard') do
  expect(page).to have_content('Dashboard')
end

The World object serves as the execution context, maintaining state between steps within a scenario. Each scenario runs with a fresh World instance, preventing test pollution. Step definitions register pattern matchers that Cucumber uses to map Gherkin steps to Ruby methods.

Basic Usage

Install Cucumber through your Gemfile and generate the initial project structure using the provided generators. The framework expects specific directory conventions for organizing features and step definitions.

# Gemfile
group :test do
  gem 'cucumber'
  gem 'capybara'
  gem 'selenium-webdriver'
end
bundle exec cucumber --init

This creates the standard directory structure with features/ containing your Gherkin files and features/step_definitions/ holding the Ruby implementations. The features/support/env.rb file handles environment setup and configuration.

Feature files describe application behavior using Gherkin keywords. Each scenario represents a specific example of how the system should behave under particular conditions. Steps use Given for context setup, When for actions, and Then for expected outcomes.

# features/shopping_cart.feature
Feature: Shopping Cart Management
  
  Background:
    Given I am logged in as a customer
    
  Scenario: Adding items to cart
    Given there are products available
    When I add a "laptop" to my cart
    And I add a "mouse" to my cart
    Then my cart should contain 2 items
    And the total should be calculated correctly
    
  Scenario Outline: Bulk discount calculation
    Given I have <quantity> items in my cart
    When I proceed to checkout
    Then I should receive a <discount>% discount
    
    Examples:
      | quantity | discount |
      | 5        | 5        |
      | 10       | 10       |
      | 20       | 15       |

Step definitions use regular expressions or Cucumber expressions to capture parameters from Gherkin steps. Parameters pass directly to the step definition method, allowing dynamic behavior based on the scenario data.

# features/step_definitions/shopping_steps.rb
Given('I am logged in as a customer') do
  @user = create(:user, role: 'customer')
  visit '/login'
  fill_in 'Email', with: @user.email
  fill_in 'Password', with: @user.password
  click_button 'Log In'
end

Given('there are products available') do
  @laptop = create(:product, name: 'laptop', price: 999.99)
  @mouse = create(:product, name: 'mouse', price: 29.99)
end

When('I add a {string} to my cart') do |product_name|
  product = instance_variable_get("@#{product_name}")
  visit "/products/#{product.id}"
  click_button 'Add to Cart'
end

Then('my cart should contain {int} items') do |expected_count|
  visit '/cart'
  expect(page).to have_selector('.cart-item', count: expected_count)
end

The cucumber configuration resides in cucumber.yml or through command-line options. Common configurations include output formats, tag filtering, and profile definitions for different execution environments.

# config/cucumber.yml
default: --require features --format pretty --strict --tags "not @wip"
ci: --require features --format junit --out tmp/test-results --strict
wip: --require features --format pretty --tags @wip --wip

Advanced Usage

Cucumber provides hooks for setup and teardown operations that run at specific points during test execution. These hooks allow database cleanup, browser configuration, and environment preparation without cluttering step definitions.

# features/support/hooks.rb
Before do
  DatabaseCleaner.clean_with(:truncation)
end

Before('@javascript') do
  Capybara.current_driver = :selenium_chrome_headless
end

After('@debug') do |scenario|
  if scenario.failed?
    save_screenshot("tmp/failure_#{scenario.name.gsub(/\s+/, '_')}.png")
  end
end

Around('@slow') do |scenario, block|
  Timeout.timeout(30) do
    block.call
  end
end

Transform objects convert parameter types automatically, reducing repetitive conversion code in step definitions. These transforms handle complex object creation and parameter parsing consistently across scenarios.

# features/support/transforms.rb
ParameterType(
  name: 'user_type',
  regexp: /admin|customer|guest/,
  transformer: ->(type) { UserType.new(type) }
)

ParameterType(
  name: 'money',
  regexp: /\$(\d+(?:\.\d{2})?)/,
  transformer: ->(amount) { Money.new(amount.to_f * 100, 'USD') }
)

# Usage in step definitions
Given('I am logged in as a {user_type}') do |user_type|
  @user = create(:user, role: user_type.role)
end

When('I make a purchase of {money}') do |amount|
  @purchase = Purchase.create!(user: @user, amount: amount)
end

Custom World modules extend the execution context with domain-specific methods and helper functions. Multiple modules can be included to organize functionality by feature area or testing concern.

# features/support/world_extensions.rb
module NavigationHelpers
  def navigate_to_page(page_name)
    path = case page_name
           when 'dashboard' then dashboard_path
           when 'profile' then profile_path(@user)
           when 'settings' then settings_path
           else
             raise "Unknown page: #{page_name}"
           end
    visit path
  end
  
  def wait_for_ajax
    Timeout.timeout(10) do
      loop until page.evaluate_script('jQuery.active == 0')
    end
  end
end

module DataHelpers
  def create_test_data(type, attributes = {})
    case type
    when 'user'
      create(:user, email: generate_email, **attributes)
    when 'product'
      create(:product, name: generate_product_name, **attributes)
    end
  end
  
  private
  
  def generate_email
    "test#{SecureRandom.hex(4)}@example.com"
  end
end

World(NavigationHelpers, DataHelpers)

Data tables in scenarios pass through as Cucumber::MultilineArgument::DataTable objects with methods for accessing rows, headers, and converting to hashes or arrays. These tables enable complex data input without cluttering step text.

# In feature file
When I create users with the following details:
  | name    | email              | role     |
  | Alice   | alice@example.com  | admin    |
  | Bob     | bob@example.com    | customer |
  | Charlie | charlie@example.com| guest    |

# In step definition
When('I create users with the following details:') do |table|
  table.hashes.each do |user_attrs|
    User.create!(
      name: user_attrs['name'],
      email: user_attrs['email'],
      role: user_attrs['role']
    )
  end
end

# Alternative table processing approaches
When('I verify the pricing table:') do |table|
  expected_prices = table.rows_hash
  expected_prices.each do |product, price|
    actual_price = Product.find_by(name: product).price
    expect(actual_price.to_s).to eq(price)
  end
end

Testing Strategies

Cucumber scenarios should focus on business behavior rather than implementation details. Structure scenarios around user goals and business outcomes, avoiding low-level technical steps that make features brittle and hard to maintain.

# Good: Business-focused scenario
Scenario: Customer places order successfully
  Given I have items in my shopping cart
  When I complete the checkout process
  Then I should receive an order confirmation
  And the items should be marked as sold

# Poor: Implementation-focused scenario  
Scenario: Order creation through API
  Given I POST to "/api/orders" with cart_id 123
  When the OrderProcessor.process method runs
  Then the database should have a new Order record
  And the inventory should decrement by 1

Tag-based organization enables selective test execution and scenario grouping. Tags control which scenarios run in different environments, execution speeds, and feature states.

Feature: Payment Processing
  
  @critical @smoke
  Scenario: Credit card payment
    # High-priority scenario for smoke tests
    
  @slow @integration  
  Scenario: Bank transfer processing
    # Longer-running integration test
    
  @wip @javascript
  Scenario: Real-time payment status
    # Work-in-progress with JavaScript requirements
# Execute specific tag combinations
# cucumber --tags "@smoke and not @slow"
# cucumber --tags "@javascript or @integration"
# cucumber --tags "not @wip"

Page Object patterns encapsulate user interface interactions within reusable classes, keeping step definitions clean and maintainable. Page objects handle element location, user actions, and state verification.

# features/support/pages/checkout_page.rb
class CheckoutPage
  include Capybara::DSL
  
  def fill_billing_info(billing_data)
    within('#billing-form') do
      fill_in 'First Name', with: billing_data[:first_name]
      fill_in 'Last Name', with: billing_data[:last_name]
      fill_in 'Address', with: billing_data[:address]
      select billing_data[:state], from: 'State'
    end
  end
  
  def select_payment_method(method)
    choose method
    case method
    when 'Credit Card'
      fill_credit_card_details
    when 'PayPal'
      click_link 'Connect PayPal'
    end
  end
  
  def complete_purchase
    click_button 'Complete Purchase'
    wait_for_confirmation
  end
  
  private
  
  def fill_credit_card_details
    fill_in 'Card Number', with: '4111111111111111'
    fill_in 'Expiry', with: '12/25'
    fill_in 'CVV', with: '123'
  end
  
  def wait_for_confirmation
    expect(page).to have_content('Order Confirmed', wait: 10)
  end
end

# Step definition using page object
When('I complete the checkout process') do
  checkout_page = CheckoutPage.new
  checkout_page.fill_billing_info(
    first_name: 'John',
    last_name: 'Doe', 
    address: '123 Main St',
    state: 'California'
  )
  checkout_page.select_payment_method('Credit Card')
  checkout_page.complete_purchase
end

Scenario contexts maintain complex state across multiple steps without polluting the global World object. This approach keeps related data grouped while preserving scenario isolation.

# features/support/contexts/order_context.rb
class OrderContext
  attr_accessor :customer, :products, :cart, :order, :payment_method
  
  def initialize
    @products = []
    @cart = Cart.new
  end
  
  def add_product(product_name, quantity = 1)
    product = Product.find_by(name: product_name)
    @cart.add_item(product, quantity)
    @products << product
  end
  
  def calculate_expected_total
    @cart.items.sum { |item| item.price * item.quantity }
  end
  
  def process_order
    @order = Order.create!(
      customer: @customer,
      cart: @cart,
      payment_method: @payment_method
    )
  end
end

# In World
module OrderHelpers
  def order_context
    @order_context ||= OrderContext.new
  end
end

World(OrderHelpers)

Common Pitfalls

Step definition ambiguity occurs when multiple patterns match the same Gherkin step, causing Cucumber to raise ambiguous match errors. Design step patterns with sufficient specificity to avoid conflicts while maintaining readability.

# Problematic: These patterns conflict
Given('I have a product') do
  # Creates any product
end

Given('I have a {string} product') do |type|
  # Creates specific product type
end

# Better: More specific patterns
Given('I have a basic product available') do
  @product = create(:product, type: 'basic')
end

Given('I have a {string} category product') do |category|
  @product = create(:product, category: category)
end

State management between steps requires careful attention to variable scope and object lifecycle. Instance variables persist within scenarios but reset between scenarios, while class variables persist across all scenarios and cause test pollution.

# Dangerous: Class variables leak between scenarios
class UserSteps
  @@current_user = nil  # Persists across scenarios
  
  def login_user(email)
    @@current_user = User.find_by(email: email)
  end
end

# Safe: Instance variables reset per scenario
Given('I am logged in as {string}') do |email|
  @current_user = User.find_by(email: email)  # Fresh per scenario
  visit '/login'
  fill_in 'Email', with: @current_user.email
end

# Better: Use World methods for complex state
module UserHelpers
  def current_user
    @current_user ||= create(:user)
  end
  
  def login_as(user)
    @current_user = user
    # Login implementation
  end
end

World(UserHelpers)

Database state management requires explicit cleanup strategies to prevent test interference. Transaction rollback works for simple cases, but complex scenarios with background jobs or external services need truncation approaches.

# features/support/database.rb
require 'database_cleaner'

DatabaseCleaner.strategy = :transaction

Before do
  DatabaseCleaner.start
end

After do |scenario|
  # Rollback for most scenarios
  DatabaseCleaner.clean
  
  # Special handling for scenarios that commit transactions
  if scenario.source_tag_names.include?('@commits_data')
    DatabaseCleaner.clean_with(:truncation)
  end
end

# Handle background job scenarios
Before('@background_jobs') do
  DatabaseCleaner.strategy = :truncation
end

After('@background_jobs') do
  DatabaseCleaner.strategy = :transaction
end

Step argument capture becomes complex with Cucumber expressions versus regular expressions. Cucumber expressions provide type safety and parameter conversion, while regex offers more flexibility for complex patterns.

# Cucumber expression (recommended)
Given('I wait {int} seconds') do |seconds|
  sleep(seconds)  # Automatic integer conversion
end

Given('the user {word} has {int} notifications') do |username, count|
  user = User.find_by(username: username)
  count.times { create(:notification, user: user) }
end

# Regular expression (for complex patterns)
Given(/^I fill in the form with:$/) do |table|
  table.rows_hash.each do |field, value|
    case field.downcase
    when /date/
      fill_in field, with: Date.parse(value)
    when /email/
      fill_in field, with: normalize_email(value)
    else
      fill_in field, with: value
    end
  end
end

Advanced Usage

Custom formatters provide specialized output for different reporting needs, from detailed HTML reports to integration with external systems. Formatters receive events during test execution and generate output accordingly.

# lib/cucumber/custom_formatter.rb
require 'cucumber/formatter/io'

class DetailedHtmlFormatter
  include Cucumber::Formatter::Io
  
  def initialize(config)
    @config = config
    @io = ensure_io(config.out_stream)
    @scenarios = []
  end
  
  def on_test_run_started(event)
    @start_time = Time.now
    @io.puts '<html><head><title>Test Results</title></head><body>'
  end
  
  def on_test_step_finished(event)
    test_step = event.test_step
    result = event.result
    
    duration = result.duration.nanoseconds / 1_000_000_000.0
    
    @io.puts "<div class='step #{result.class.name.downcase}'>"
    @io.puts "  <span class='step-text'>#{test_step.text}</span>"
    @io.puts "  <span class='duration'>#{duration.round(3)}s</span>"
    
    if result.failed?
      @io.puts "  <div class='error'>#{result.exception.message}</div>"
    end
    
    @io.puts "</div>"
  end
  
  def on_test_run_finished(event)
    total_duration = Time.now - @start_time
    @io.puts "<div class='summary'>Total: #{total_duration.round(2)}s</div>"
    @io.puts '</body></html>'
  end
end

Cucumber expressions support complex parameter matching with custom parameter types, allowing sophisticated data conversion and validation directly in step patterns.

# features/support/parameter_types.rb
ParameterType(
  name: 'json',
  regexp: /\{.*\}/,
  transformer: ->(json_string) { JSON.parse(json_string) }
)

ParameterType(
  name: 'table_record',
  regexp: /(\w+):\s*(.+)/,
  transformer: ->(model, attributes) {
    model_class = model.classify.constantize
    attrs = attributes.split(',').map { |attr| 
      key, value = attr.split(':')
      [key.strip.to_sym, value.strip]
    }.to_h
    model_class.new(attrs)
  }
)

# Usage in steps
When('I send the request {json}') do |json_data|
  post '/api/endpoint', json_data
end

Given('I have a {table_record}') do |record|
  record.save!
  instance_variable_set("@#{record.class.name.downcase}", record)
end

Background steps run before each scenario in a feature, establishing common context without repetition. Background steps execute in the same World instance as the scenario steps, sharing state and setup.

Feature: User Account Management
  
  Background:
    Given the application is running
    And I have admin privileges
    And there are existing user accounts
    
  Scenario: Updating user information
    When I modify a user's email address
    Then the change should be reflected in the database
    And the user should receive a confirmation email
    
  Scenario: Deactivating user accounts  
    When I deactivate a user account
    Then the user should not be able to log in
    And their sessions should be terminated

Production Patterns

Continuous integration environments require specific Cucumber configuration for reliable execution, including browser management, test data isolation, and result reporting for integration with build systems.

# features/support/ci_env.rb
if ENV['CI']
  require 'selenium-webdriver'
  
  Capybara.register_driver :ci_chrome do |app|
    options = Selenium::WebDriver::Chrome::Options.new
    options.add_argument('--headless')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--disable-gpu')
    options.add_argument('--window-size=1920,1080')
    
    Capybara::Selenium::Driver.new(
      app,
      browser: :chrome,
      options: options
    )
  end
  
  Capybara.default_driver = :ci_chrome
  Capybara.javascript_driver = :ci_chrome
  Capybara.default_max_wait_time = 10
end

Parallel execution reduces test suite runtime by distributing scenarios across multiple processes. The parallel_cucumber gem handles scenario distribution and result aggregation automatically.

# Gemfile
gem 'parallel_cucumber'

# cucumber.yml
parallel: --require features --format pretty --format junit --out tmp/cucumber-results
# features/support/parallel_setup.rb
Before do
  if ENV['TEST_ENV_NUMBER']
    # Unique database per parallel process
    db_suffix = ENV['TEST_ENV_NUMBER'] == '' ? '0' : ENV['TEST_ENV_NUMBER']
    config = Rails.application.config.database_configuration['test']
    config['database'] = "#{config['database']}_#{db_suffix}"
    ActiveRecord::Base.establish_connection(config)
  end
end

Profile-based execution configurations handle different testing contexts through cucumber.yml profiles. Profiles define environment-specific settings for development, staging, and production testing scenarios.

# config/cucumber.yml
default: features --format pretty --strict --tags "not @wip and not @slow"

development: features --format pretty --tags "not @wip" --require features

staging: >
  features 
  --format pretty 
  --format html --out tmp/cucumber_report.html
  --tags "not @local_only"
  --require features

smoke: >
  features
  --format progress
  --tags "@smoke"
  --fail-fast
  --require features

full: >
  features
  --format pretty
  --format junit --out tmp/junit
  --format html --out tmp/cucumber_report.html  
  --strict
  --require features

Feature file organization scales through careful directory structure and naming conventions. Group related features together while maintaining clear boundaries between different application areas.

features/
├── authentication/
│   ├── login.feature
│   ├── password_reset.feature
│   └── registration.feature
├── shopping/
│   ├── cart_management.feature
│   ├── checkout_process.feature
│   └── product_browsing.feature
├── admin/
│   ├── user_management.feature
│   └── system_configuration.feature
└── step_definitions/
    ├── authentication_steps.rb
    ├── shopping_steps.rb
    └── admin_steps.rb

Reference

Core Classes and Methods

Class Purpose Key Methods
Cucumber::Core::Test::Step Individual step execution #text, #location, #match
Cucumber::Core::Test::Result Step execution outcome #passed?, #failed?, #exception
Cucumber::MultilineArgument::DataTable Table parameter handling #hashes, #rows, #transpose
Cucumber::Glue::StepDefinition Step pattern registration #match, #invoke, #expression

Data Table Methods

Method Parameters Returns Description
#hashes None Array<Hash> Converts table to array of hashes using first row as keys
#rows None Array<Array> Returns raw table data as nested arrays
#rows_hash None Hash Converts two-column table to hash
#transpose None DataTable Flips rows and columns
#map_headers! Block DataTable Transforms header names using block
#map_column! String/Integer, Block DataTable Transforms specific column values

Hook Types and Execution Order

Hook Execution Point Usage Pattern
Before Before each scenario Setup, authentication, data preparation
After After each scenario Cleanup, screenshots, logging
Around Wraps scenario execution Timeout, transactions, environment changes
BeforeStep Before each step Debugging, state verification
AfterStep After each step Screenshot capture, step logging
InstallPlugin Cucumber startup Plugin registration, global setup

Tag Filtering Syntax

Expression Matches Description
@smoke Single tag Scenarios tagged with @smoke
@smoke and @quick Both tags Scenarios with both tags
@smoke or @regression Either tag Scenarios with at least one tag
not @slow Tag absence Scenarios without @slow tag
(@smoke or @quick) and not @slow Complex logic Combined expressions with precedence

Configuration Options

Option Type Default Description
--format String pretty Output formatter selection
--out String STDOUT Output destination file
--require String features Ruby file loading path
--tags String None Tag expression for filtering
--strict Boolean false Fail on undefined or pending steps
--dry-run Boolean false Parse without executing steps
--fail-fast Boolean false Stop on first failure
--retry Integer 0 Retry failed scenarios count

Step Definition Patterns

Pattern Type Syntax Example Use Case
Cucumber Expression {int}, {string}, {word} I have {int} items Type-safe parameter capture
Regular Expression /pattern/ /I have (\d+) items/ Complex pattern matching
String Literal 'exact text' 'I click the submit button' Exact step matching

Common Parameter Types

Type Pattern Transforms To Example
{int} -?\d+ Integer {int} items42
{float} -?\d*\.?\d+ Float {float} price19.99
{word} [^\s]+ String {word} status"active"
{string} "([^"]*)" String {string} name"John Doe"

Error Types and Resolution

Error Cause Resolution
Cucumber::Ambiguous Multiple step definitions match Make patterns more specific
Cucumber::Undefined No step definition found Implement missing step definition
Cucumber::Pending Step marked as pending Complete step implementation
Capybara::ElementNotFound Element selector fails Add explicit waits or fix selectors
ActiveRecord::RecordNotFound Missing test data Verify data setup in Given steps

World Object Lifecycle

Phase Description Available Objects
Before Hook Pre-scenario setup Fresh World instance, shared helper modules
Scenario Execution Step-by-step execution Instance variables, World methods, parameter data
After Hook Post-scenario cleanup All scenario state, test results, exception data

Browser Driver Configuration

Driver Use Case Configuration
:rack_test Fast non-JavaScript tests Default, no browser
:selenium_chrome JavaScript-enabled tests Full Chrome browser
:selenium_chrome_headless CI environments Chrome without GUI
:cuprite Fast JavaScript tests Headless Chrome via CDP