Step Definitions
View SourceStep definitions connect the Gherkin steps in your feature files to actual code. They're the glue between your natural language specifications and the implementation that tests your application.
Creating Step Definition Files
Step definitions should be placed in test/features/step_definitions/ with a .exs extension:
# test/features/step_definitions/authentication_steps.exs
defmodule AuthenticationSteps do
use Cucumber.StepDefinition
import ExUnit.Assertions
# Step definitions go here
endBasic Step Definition
Step definitions are created using the step macro:
step "I am logged in as a customer", context do
# Authentication logic here
Map.put(context, :user, create_and_login_customer())
endSteps with Parameters
Cucumber supports several parameter types that can be used in step patterns. Parameters are accessed through pattern matching on the args field of the context:
String Parameters
step "I am on the product page for {string}", %{args: [product_name]} do
# Navigate to product page
%{current_page: :product, product_name: product_name}
endInteger Parameters
step "I should have {int} items in my wishlist", %{args: [expected_count]} = context do
# Assertion for wishlist count
assert get_wishlist_count(context) == expected_count
context
endFloat Parameters
step "the total price should be {float}", %{args: [expected_total]} = context do
# Assertion for price
assert_in_delta get_cart_total(context), expected_total, 0.01
context
endWord Parameters
step "I should see the {word} dashboard", %{args: [dashboard_type]} = context do
# Assertion for dashboard type
assert get_current_dashboard(context) == dashboard_type
context
endAtom Parameters
step "the order status is {atom}", %{args: [status]} = context do
# status is an atom like :pending, :shipped, :delivered
assert get_order_status(context) == status
context
endMultiple Parameters
When a step has multiple parameters, you can pattern match on all of them:
step "I transfer {float} from {string} to {string}", %{args: [amount, from_account, to_account]} do
# Transfer logic
%{transfer: %{amount: amount, from: from_account, to: to_account}}
endOptional Parameters
Use ? after a parameter type to make it optional. Returns nil if not present:
step "I have {int?} items", %{args: [count]} = context do
# count is an integer or nil
actual_count = count || 0
Map.put(context, :item_count, actual_count)
endOptional Text
Use (text) to mark text as optional. Optional text is not captured:
step "the following group(s) exist:", context do
# Matches "the following group exist:" or "the following groups exist:"
context
end
step "I have {int} cucumber(s)", %{args: [count]} = context do
# Matches "I have 1 cucumber" or "I have 5 cucumbers"
Map.put(context, :cucumber_count, count)
endAlternation
Use word1/word2 (no spaces around /) to match alternative words. Alternations are not captured:
step "I click/tap the {string} button", %{args: [button_name]} = context do
# Matches "I click the..." or "I tap the..."
# Only button_name is captured, not the click/tap choice
Map.put(context, :clicked_button, button_name)
end
step "I have/own {int} items", %{args: [count]} = context do
# Matches "I have 5 items" or "I own 5 items"
Map.put(context, :item_count, count)
endEscape Sequences
Use backslash to escape special characters:
\{and\}for literal braces\(and\)for literal parentheses\/for literal forward slash\\for literal backslash
step "I see \\{placeholder\\} text", context do
# Matches "I see {placeholder} text"
context
end
step "call\\(\\)", context do
# Matches "call()"
context
end
step "path\\/to\\/file", context do
# Matches "path/to/file" (without treating / as alternation)
context
endWorking with Data Tables
In your feature file:
Given I have the following items in my cart:
| Product Name | Quantity | Price |
| Smartphone | 1 | 699.99|
| Protection Plan | 1 | 79.99 |In your step definitions:
step "I have the following items in my cart:", context do
# Access the datatable
datatable = context.datatable
# Access headers
headers = datatable.headers # ["Product Name", "Quantity", "Price"]
# Access rows as maps
items = datatable.maps
# [
# %{"Product Name" => "Smartphone", "Quantity" => "1", "Price" => "699.99"},
# %{"Product Name" => "Protection Plan", "Quantity" => "1", "Price" => "79.99"}
# ]
# Process the items
Map.put(context, :cart_items, items)
endWorking with DocStrings
DocStrings allow you to pass multi-line text to a step:
In your feature file:
When I submit the following JSON:
"""
{
"name": "Test Product",
"price": 29.99,
"available": true
}
"""In your step definitions:
step "I submit the following JSON:", context do
# The docstring is available in context.docstring
json_data = Jason.decode!(context.docstring)
# Process the JSON
Map.put(context, :submitted_data, json_data)
endReturn Values
Step definitions must return one of the following values (matching ExUnit's setup behavior):
:ok- Keeps the context unchanged- A map - Merged into the existing context
- A keyword list - Merged into the existing context
{:ok, map_or_keyword_list}- Merged into the existing context{:error, reason}- Fails the step with the given reason
Reusable Step Definitions
You can create reusable step definitions that can be shared across multiple features:
# test/features/step_definitions/common_steps.exs
defmodule CommonSteps do
use Cucumber.StepDefinition
import ExUnit.Assertions
step "I wait {int} seconds", %{args: [seconds]} = context do
Process.sleep(seconds * 1000)
context
end
step "I should see {string}", %{args: [text]} = context do
assert page_contains_text?(context, text)
context
end
endBest Practices
- Keep steps focused: Each step should do one thing well
- Use descriptive step patterns: Make your steps readable and self-documenting
- Share common steps: Create reusable step definitions for common actions
- Handle errors gracefully: Return
{:error, reason}for expected failures - Maintain context: Always return the context (or
:ok) to maintain state between steps