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} items → 42 |
{float} |
-?\d*\.?\d+ |
Float |
{float} price → 19.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 |