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.
Basic Step Definition
Step definitions are created using the defstep
macro:
defstep "I am logged in as a customer", context do
# Authentication logic here
{:ok, %{user: create_and_login_customer()}}
end
Steps with Parameters
Cucumber supports several parameter types that can be used in step patterns:
String Parameters
defstep "I am on the product page for {string}", context do
product_name = List.first(context.args)
# Navigate to product page
{:ok, %{current_page: :product, product_name: product_name}}
end
Integer Parameters
defstep "I should have {int} items in my wishlist", context do
expected_count = List.first(context.args)
# Assertion for wishlist count
assert get_wishlist_count() == expected_count
:ok
end
Float Parameters
defstep "the total price should be {float}", context do
expected_total = List.first(context.args)
# Assertion for price
assert_in_delta get_cart_total(), expected_total, 0.01
:ok
end
Word Parameters
defstep "I should see the {word} dashboard", context do
dashboard_type = List.first(context.args)
# Assertion for dashboard type
assert get_current_dashboard() == dashboard_type
:ok
end
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 test module:
defstep "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
{:ok, %{cart_items: items}}
end
Working with Doc Strings
In your feature file:
When I submit the following feedback:
"""
I really like your product, but I think
it could be improved by adding more features.
Keep up the good work!
"""
In your test module:
defstep "I submit the following feedback:", context do
feedback_text = context.docstring
# Submit feedback logic
{:ok, %{submitted_feedback: feedback_text}}
end
Return Values
Step definitions can return values in several ways:
1. Return :ok
For steps that perform actions but don't need to update context:
defstep "I click the submit button", _context do
# Click logic
:ok
end
2. Return a Map
To directly replace the context:
defstep "I am on the home page", _context do
%{current_page: :home}
end
3. Return {:ok, map}
To merge new values into the context:
defstep "I search for {string}", context do
search_term = List.first(context.args)
# Search logic
{:ok, %{search_term: search_term, search_results: perform_search(search_term)}}
end
4. Return {:error, reason}
To indicate a step failure with a reason:
defstep "the payment should be successful", context do
if context.payment_status == :success do
:ok
else
{:error, "Expected payment to succeed, but got status: #{context.payment_status}"}
end
end
Step Patterns and Matching
When a Gherkin step needs to be executed, the framework searches through all step definitions for a matching pattern. The matching process:
- Starts with the current module's step definitions
- Looks for an exact match first
- Then tries to match using parameter placeholders
- Raises an error if no matching step definition is found
Context Management
The context object is a map that flows from step to step during a scenario's execution:
- It starts as an empty map or the ExUnit test context
- Each step can add to, modify, or replace the context
- Any values added to the context are available to subsequent steps
- It's useful for sharing state between steps, such as user sessions, form data, etc.
Example of context flow:
defstep "I am on the login page", _context do
# Initial step sets up the page
%{page: :login}
end
defstep "I enter my credentials", context do
# Second step uses the page from previous step and adds credentials
assert context.page == :login
{:ok, %{username: "testuser", password: "password123"}}
end
defstep "I click the login button", context do
# Third step can access all previous context values
assert context.page == :login
assert context.username == "testuser"
assert context.password == "password123"
# And add more context values
{:ok, %{logged_in: true, user_id: 123}}
end
Best Practices for Step Definitions
- Keep steps reusable - Write generic steps that can be used across features
- One assertion per step - Especially for "Then" steps
- Use context for state management - Pass necessary data between steps
- Handle errors gracefully - Provide helpful error messages
- Name steps from the user's perspective - Focus on what, not how
- Organize steps logically - Group related steps together
- Document complex steps - Add comments for steps with complex logic
- Avoid implementation details in step patterns - Keep to business terminology