Step Definitions

View Source

Step 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
end

Basic 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())
end

Steps 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}
end

Integer 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
end

Float 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
end

Word 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
end

Atom 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
end

Multiple 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}}
end

Optional 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)
end

Optional 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)
end

Alternation

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)
end

Escape 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
end

Custom 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/
end

Then 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)
end

With 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)
end

If 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)
end

Working 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)
end

Return 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
  • :pending or {:pending, message} - Marks the step as not yet implemented: the remaining steps are skipped, after hooks still run, and the scenario fails with Cucumber.PendingStepError
  • :skipped or {: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
end

Best Practices

  1. Keep steps focused: Each step should do one thing well
  2. Use descriptive step patterns: Make your steps readable and self-documenting
  3. Share common steps: Create reusable step definitions for common actions
  4. Handle errors gracefully: Return {:error, reason} for expected failures
  5. Maintain context: Always return the context (or :ok) to maintain state between steps