Testing with Cucumber
Testing often feels like a barrier between developers and stakeholders, creating confusion about what software should actually do. Cucumber bridges this gap by using plain English specifications that both technical and non-technical team members can understand and contribute to.
Cucumber transforms requirements into executable tests using Gherkin syntax, a structured language that reads like natural conversation. This approach ensures everyone shares the same understanding of how your application should behave, reducing miscommunication and missed requirements.
This guide will walk you through setting up Cucumber from scratch, writing your first scenarios, and building a complete behavior-driven development workflow.
Prerequisites
To follow this guide, you'll need Ruby 3.0 or later installed:
ruby --version
ruby 3.4.5
This guide assumes familiarity with Ruby basics including methods, classes, and object-oriented programming. You should also understand fundamental testing concepts, though no prior experience with behavior-driven development is required.
Setting up the project
In this section, you’ll explore Cucumber with hands-on examples that bring behavior-driven development to life. A dedicated project environment lets you safely experiment with its syntax and workflow without impacting production code.
Create a project directory for Cucumber exploration:
mkdir cucumber-testing-guide && cd cucumber-testing-guide
Initialize a Gemfile for dependency management:
bundle init
Add Cucumber and supporting gems to your Gemfile:
source "https://rubygems.org"
gem 'cucumber', '~> 10.1'
gem 'rspec-expectations', '~> 3.13'
Install the dependencies:
bundle install
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies...
...
Fetching cucumber 10.1.0
Installing cucumber 10.1.0
Bundle complete! 2 Gemfile dependencies, 21 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
1 installed gem you directly depend on is looking for funding.
Run `bundle fund` for details
Initialize the Cucumber directory structure:
bundle exec cucumber --init
create features
create features/step_definitions
create features/support
create features/support/env.rb
This creates a standard Cucumber project structure with directories for features, step definitions, and support files. The env.rb
file contains configuration that loads before any features run.
Your Cucumber environment is now ready for behavior-driven development exploration.
Understanding the behavior-driven development challenge
Traditional testing focuses on implementation details rather than user behavior, creating tests that break when code changes even if the functionality remains correct. This approach makes it difficult for stakeholders to understand what the software actually does.
Consider a simple login system. Traditional unit tests might verify that a User
class has an authenticate
method:
def test_authenticate_method_exists
user = User.new
assert user.respond_to?(:authenticate)
end
This test tells us nothing about how authentication should work from a user's perspective. What happens when credentials are correct? What about invalid passwords? Should the system lock accounts after failed attempts?
Behavior-driven development addresses this by focusing on user scenarios instead of code implementation. Rather than testing methods exist, you describe what users should experience:
- When a user enters correct credentials, they should be logged in
- When a user enters wrong credentials, they should see an error message
- When a user fails to log in multiple times, their account should be temporarily locked
These scenarios become executable tests that verify your application works correctly from the user's perspective, regardless of how the underlying code is structured.
Writing your first Cucumber scenario
Cucumber uses Gherkin syntax to write scenarios that read like natural language. Each scenario describes a specific user interaction with your application using Given-When-Then steps.
Create your first feature file in the features
direcotry to describe basic calculator functionality:
Feature: Basic Calculator Operations
As a user
I want to perform basic arithmetic operations
So that I can calculate results quickly
Scenario: Adding two positive numbers
Given I have a calculator
When I add 5 and 3
Then the result should be 8
Gherkin scenarios follow a specific structure that makes them easy to understand:
Feature
describes the overall functionality being testedAs/I want/So that
explains who benefits and why this mattersScenario
defines a specific situation or use caseGiven
sets up the initial contextWhen
describes the action being performedThen
specifies the expected outcome
The beauty of Gherkin lies in its readability. Anyone on your team can understand what this scenario tests without knowing how calculators are implemented.
Run Cucumber to see what happens with undefined steps:
bundle exec cucumber
Feature: Basic Calculator Operations
As a user
I want to perform basic arithmetic operations
So that I can calculate results quickly
Scenario: Adding two positive numbers # features/calculator.feature:6
Given I have a calculator # features/calculator.feature:7
When I add 5 and 3 # features/calculator.feature:8
Then the result should be 8 # features/calculator.feature:9
1 scenario (1 undefined)
3 steps (3 undefined)
0m0.015s
You can implement step definitions for undefined steps with these snippets:
Given('I have a calculator') do
pending # Write code here that turns the phrase above into concrete actions
end
When('I add {int} and {int}') do |int, int2|
# When('I add {int} and {float}') do |int, float|
# When('I add {float} and {int}') do |float, int|
# When('I add {float} and {float}') do |float, float2|
pending # Write code here that turns the phrase above into concrete actions
end
Then('the result should be {int}') do |int|
# Then('the result should be {float}') do |float|
pending # Write code here that turns the phrase above into concrete actions
end
...
Cucumber automatically identifies undefined steps and suggests Ruby code snippets to implement them. Notice how it recognizes numbers in the steps and converts them to parameters.
Implementing step definitions
Step definitions connect Gherkin scenarios to Ruby code that actually performs the described actions. Each step definition uses pattern matching to capture parameters from scenario text.
Create step definitions for the calculator scenario:
Given('I have a calculator') do
@calculator = Calculator.new
end
When('I add {int} and {int}') do |first_number, second_number|
@result = @calculator.add(first_number, second_number)
end
Then('the result should be {int}') do |expected_result|
expect(@result).to eq(expected_result)
end
Step definitions use instance variables to share data between steps within the same scenario. The @calculator
created in the Given step remains available in subsequent When and Then steps.
Cucumber automatically converts {int}
parameters to Ruby integers, eliminating the need for manual string-to-number conversion. This parameter extraction makes steps reusable across different scenarios with various values.
Create the Calculator class to support these step definitions:
class Calculator
def add(a, b)
a + b
end
end
Update the support file to load your application code:
require_relative '../../lib/calculator'
require 'rspec/expectations'
The env.rb
file loads before any features run, making it the perfect place to require application files and configure testing libraries like RSpec expectations.
Run the scenario again:
bundle exec cucumber
Feature: Basic Calculator Operations
As a user
I want to perform basic arithmetic operations
So that I can calculate results quickly
Scenario: Adding two positive numbers # features/calculator.feature:6
Given I have a calculator # features/step_definitions/calculator_steps.rb:1
When I add 5 and 3 # features/step_definitions/calculator_steps.rb:5
Then the result should be 8 # features/step_definitions/calculator_steps.rb:9
1 scenario (1 passed)
3 steps (3 passed)
0m0.001s
The scenario now passes completely, demonstrating how Cucumber connects human-readable specifications to executable code that verifies your application works correctly. you can also see colors... exploian about colors
Cucumber provides color-coded output that makes test results immediately clear. Passing steps appear in green, while failing steps show up in red, and pending or undefined steps display in yellow. This visual feedback lets you quickly scan results and identify which scenarios need attention without reading through detailed text output.
The color coding becomes especially valuable when running large test suites where dozens of scenarios execute at once. You can immediately spot problems by looking for red text, while green confirms everything works as expected. This visual approach reduces the cognitive load of interpreting test results and helps maintain focus during development cycles.
Building comprehensive scenario coverage
Real applications require testing multiple scenarios to ensure they handle various user interactions correctly. Cucumber's scenario structure makes it easy to describe different situations systematically.
Expand the feature file to cover additional calculator operations:
Feature: Basic Calculator Operations
As a user
I want to perform basic arithmetic operations
So that I can calculate results quickly
Scenario: Adding two positive numbers
Given I have a calculator
When I add 5 and 3
Then the result should be 8
Scenario: Adding negative numbers
Given I have a calculator
When I add -2 and -3
Then the result should be -5
Scenario: Subtracting numbers
Given I have a calculator
When I subtract 7 from 10
Then the result should be 3
Scenario: Multiplying numbers
Given I have a calculator
When I multiply 4 and 6
Then the result should be 24
Scenario: Dividing numbers
Given I have a calculator
When I divide 15 by 3
Then the result should be 5
Each scenario tests a specific operation while following the same Given-When-Then structure. This consistency makes scenarios easy to read and understand, even as the feature file grows larger.
Add corresponding step definitions for the new operations:
Given('I have a calculator') do
@calculator = Calculator.new
end
When('I add {int} and {int}') do |first_number, second_number|
@result = @calculator.add(first_number, second_number)
end
When('I subtract {int} from {int}') do |subtrahend, minuend|
@result = @calculator.subtract(minuend, subtrahend)
end
When('I multiply {int} and {int}') do |first_number, second_number|
@result = @calculator.multiply(first_number, second_number)
end
When('I divide {int} by {int}') do |dividend, divisor|
@result = @calculator.divide(dividend, divisor)
end
Then('the result should be {int}') do |expected_result|
expect(@result).to eq(expected_result)
end
Notice how the Given and Then steps remain unchanged, demonstrating how well-designed step definitions can be reused across multiple scenarios. Only the When steps need to be added for new operations.
Update the Calculator class to support all operations:
class Calculator
def add(a, b)
a + b
end
def subtract(a, b)
a - b
end
def multiply(a, b)
a * b
end
def divide(a, b)
a / b
end
end
Run the expanded test suite:
bundle exec cucumber
Feature: Basic Calculator Operations
As a user
I want to perform basic arithmetic operations
So that I can calculate results quickly
Scenario: Adding two positive numbers # features/calculator.feature:6
Given I have a calculator # features/step_definitions/calculator_steps.rb:1
When I add 5 and 3 # features/step_definitions/calculator_steps.rb:5
Then the result should be 8 # features/step_definitions/calculator_steps.rb:21
Scenario: Adding negative numbers # features/calculator.feature:10
Given I have a calculator # features/step_definitions/calculator_steps.rb:1
When I add -2 and -3 # features/step_definitions/calculator_steps.rb:5
Then the result should be -5 # features/step_definitions/calculator_steps.rb:21
Scenario: Subtracting numbers # features/calculator.feature:15
Given I have a calculator # features/step_definitions/calculator_steps.rb:1
When I subtract 7 from 10 # features/step_definitions/calculator_steps.rb:9
Then the result should be 3 # features/step_definitions/calculator_steps.rb:21
Scenario: Multiplying numbers # features/calculator.feature:20
Given I have a calculator # features/step_definitions/calculator_steps.rb:1
When I multiply 4 and 6 # features/step_definitions/calculator_steps.rb:13
Then the result should be 24 # features/step_definitions/calculator_steps.rb:21
Scenario: Dividing numbers # features/calculator.feature:25
Given I have a calculator # features/step_definitions/calculator_steps.rb:1
When I divide 15 by 3 # features/step_definitions/calculator_steps.rb:17
Then the result should be 5 # features/step_definitions/calculator_steps.rb:21
5 scenarios (5 passed)
15 steps (15 passed)
0m0.005s
All scenarios pass, demonstrating how Cucumber scenarios can efficiently cover multiple aspects of your application's behavior while remaining readable and maintainable.
Using scenario outlines for data-driven tests
When you need to test the same behavior with different data sets, Scenario Outlines eliminate repetition by parameterizing scenarios with example tables.
Create a feature that tests multiple calculations with various inputs:
Feature: Multiplication Table
As a student
I want to verify multiplication results
So that I can check my homework answers
Scenario Outline: Multiplying different numbers
Given I have a calculator
When I multiply <first_number> and <second_number>
Then the result should be <expected_result>
Examples:
| first_number | second_number | expected_result |
| 2 | 3 | 6 |
| 4 | 5 | 20 |
| 7 | 8 | 56 |
| 9 | 9 | 81 |
| 12 | 11 | 132 |
Scenario Outlines use placeholders in angle brackets that get replaced with values from the Examples table. Cucumber runs the scenario once for each row in the table, creating multiple test cases from a single scenario definition.
The Examples table makes it easy to add new test cases without writing additional scenarios. You can also include edge cases, boundary values, or regression test data in the same table.
Run the multiplication table feature:
bundle exec cucumber features/multiplication_table.feature
Feature: Multiplication Table
As a student
I want to verify multiplication results
So that I can check my homework answers
Scenario Outline: Multiplying different numbers # features/multiplication_table.feature:6
Given I have a calculator # features/multiplication_table.feature:7
When I multiply <first_number> and <second_number> # features/multiplication_table.feature:8
Then the result should be <expected_result> # features/multiplication_table.feature:9
Examples:
| first_number | second_number | expected_result |
| 2 | 3 | 6 |
| 4 | 5 | 20 |
| 7 | 8 | 56 |
| 9 | 9 | 81 |
| 12 | 11 | 132 |
5 scenarios (5 passed)
15 steps (15 passed)
0m0.005s
Notice how the Examples table displays clearly in the terminal output with proper column alignment. Each row represents a separate test case that Cucumber executes automatically. The table format makes it easy to see the relationship between input values and expected results at a glance.
The terminal preserves the table structure from your feature file, maintaining readability even in text-based output. This makes it simple to verify which specific data combinations were tested and identify patterns in your test data.
Cucumber automatically generates five separate scenarios from the single Scenario Outline, each with different parameter values. This approach makes it easy to test edge cases and verify your application handles various inputs correctly.
Running and filtering scenarios
Cucumber provides various options for running specific scenarios during development, making it easy to focus on particular functionality.
Running specific features and scenarios
Run individual feature files:
bundle exec cucumber features/calculator.feature
Run specific scenarios by line number:
bundle exec cucumber features/calculator.feature:16
This runs only the scenario that begins on line 16, useful when debugging specific test failures.
Final thoughts
In this article, you learned how Cucumber turns requirements into living documentation that you can understand. When you write scenarios in plain language, you align everyone around the same expectations and ensure your application behaves as intended.
With features like scenario outlines and color-coded output, Cucumber makes behavior-driven development both practical and collaborative. To dive deeper, check out the official Cucumber documentation.