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.

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:

  1. Starts with the current module's step definitions
  2. Looks for an exact match first
  3. Then tries to match using parameter placeholders
  4. 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

  1. Keep steps reusable - Write generic steps that can be used across features
  2. One assertion per step - Especially for "Then" steps
  3. Use context for state management - Pass necessary data between steps
  4. Handle errors gracefully - Provide helpful error messages
  5. Name steps from the user's perspective - Focus on what, not how
  6. Organize steps logically - Group related steps together
  7. Document complex steps - Add comments for steps with complex logic
  8. Avoid implementation details in step patterns - Keep to business terminology