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
endCustom Parameter Types
Define your own parameter types in a support file (loaded before step
definitions) with Cucumber.ParameterTypes:
# test/features/support/parameter_types.exs
defmodule MyParameterTypes do
use Cucumber.ParameterTypes
parameter_type :flight,
regexp: ~r/([A-Z]{3})-([A-Z]{3})/,
transform: fn from, to -> %{from: from, to: to} end
parameter_type :priority, regexp: ~r/low|normal|high/
endThen use them like any built-in type:
step "flight {flight} is boarding", %{args: [flight]} = context do
# flight is %{from: "LHR", to: "CDG"}
Map.put(context, :route, flight)
endWith no capture groups the transform receives the full match; with capture groups it receives one argument per group. Without a transform, the parameter yields the matched string. A step pattern referencing an unregistered type is excluded with a warning, and steps that would have matched it are reported as undefined.
Regular Expression Patterns
Steps can also be defined with a regular expression instead of a cucumber
expression. The regex must match the entire step text (add your own ^/$
anchors or not — full-text matching is enforced either way). Capture groups
arrive in context.args in order, always as strings — regexes do no type
conversion — and unmatched optional groups are nil:
step ~r/^a basket holding (\d+) apples(?: and (\d+) pears)?$/, %{args: [apples, pears]} = context do
total = String.to_integer(apples) + String.to_integer(pears || "0")
Map.put(context, :basket_total, total)
endIf a step's text matches both a regex and a cucumber expression (or any two definitions), the step fails as ambiguous.
Working 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:pendingor{:pending, message}- Marks the step as not yet implemented: the remaining steps are skipped, after hooks still run, and the scenario fails withCucumber.PendingStepError:skippedor{:skipped, reason}- Skips the rest of the scenario without failing it: the remaining steps don't run, after hooks still run, and a one-line notice is printed. Useful when the environment can't support a scenario (a missing service, platform-specific behavior, etc.)
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