Best Practices for Cucumber Tests

View Source

This guide outlines best practices for writing and organizing your Cucumber tests to ensure they remain maintainable, readable, and effective.

Feature File Organization

Directory Structure

test/
 features/                    # Feature files
    authentication/          # Feature grouping by domain
       login.feature
       registration.feature
    shopping/               # Another domain
       cart.feature
       checkout.feature
    step_definitions/       # Step definition files
        authentication_steps.exs
        shopping_steps.exs
        common_steps.exs

Naming Conventions

  • Use snake_case for feature file names
  • Group related features into subdirectories
  • Name step definition modules descriptively (e.g., AuthenticationSteps, ShoppingSteps)
  • Use .exs extension for step definition files

Writing Good Scenarios

Scenario Best Practices

  1. Keep scenarios focused: Each scenario should test one specific behavior
  2. Be consistent: Use consistent language across scenarios
  3. Use concrete examples: Prefer specific, realistic values to abstract placeholders
  4. Avoid technical details: Keep scenarios in business language
  5. Keep them short: Aim for 3-7 steps per scenario
  6. Use backgrounds wisely: Only for truly common setup steps

Example: Bad vs. Good

Bad:

Scenario: User interaction
  Given a user
  When the user does stuff
  Then the outcome is good

Good:

Scenario: Customer adds product to shopping cart
  Given I am logged in as "alice@example.com"
  And I am viewing the "iPhone 15 Pro" product page
  When I click "Add to Cart"
  Then I should see "iPhone 15 Pro" in my shopping cart
  And the cart total should be $999.00

Step Definition Best Practices

Keep Steps Reusable

Write generic steps that can be reused across features:

# Good - reusable
step "I click {string}", %{args: [button_text]} = context do
  click_button(context, button_text)
end

# Less reusable - too specific
step "I click the red submit button on the login form", context do
  click_specific_button(context)
end

Use the Context Effectively

Pass data between steps using the context:

step "I create a product named {string}", %{args: [name]} = context do
  product = create_product(name: name)
  Map.put(context, :product, product)
end

step "I should see the product in my catalog", context do
  assert product_in_catalog?(context.product)
  context
end

Organize Step Definitions

Group related steps in the same module:

# test/features/step_definitions/authentication_steps.exs
defmodule AuthenticationSteps do
  use Cucumber.StepDefinition
  import ExUnit.Assertions

  # Login steps
  step "I am logged in as {string}", %{args: [email]} = context do
    user = login_user(email)
    Map.put(context, :current_user, user)
  end

  # Registration steps
  step "I register with email {string}", %{args: [email]} = context do
    user = register_user(email: email)
    Map.put(context, :new_user, user)
  end
end

Common Patterns

Data Setup Pattern

Create helper functions for common data setup:

defmodule TestHelpers do
  def create_user(attrs \\ %{}) do
    default_attrs = %{
      email: "test@example.com",
      name: "Test User"
    }

    attrs = Map.merge(default_attrs, attrs)
    # Create user logic
  end
end

# In your steps
step "a user exists with email {string}", %{args: [email]} do
  user = TestHelpers.create_user(email: email)
  %{user: user}
end

Assertion Helpers

Create custom assertion helpers for cleaner steps:

defmodule AssertionHelpers do
  import ExUnit.Assertions

  def assert_logged_in(context) do
    assert context[:current_user] != nil
    assert context[:session_token] != nil
  end

  def assert_product_visible(context, product_name) do
    assert product_name in get_visible_products(context)
  end
end

Testing Tips

Use Tags for Organization

Tag your scenarios for easy filtering:

@authentication @smoke
Scenario: Successful login
  Given I am on the login page
  When I enter valid credentials
  Then I should be logged in

@wip @slow
Scenario: Complex data processing
  Given a large dataset
  When I process the data
  Then the results should be accurate

Run specific tags:

mix test --only authentication
mix test --exclude wip

Background vs. Helper Steps

Use backgrounds for truly common setup that applies to all scenarios:

Background:
  Given the system is initialized
  And default products exist

Scenario: View product catalog
  When I visit the catalog page
  Then I should see all products

Handling Asynchronous Operations

For operations that might take time:

step "I wait for the email to arrive", context do
  # Poll for the email with a timeout
  email = wait_for_email(context.current_user.email, timeout: 5_000)
  Map.put(context, :received_email, email)
end

defp wait_for_email(email, opts) do
  timeout = Keyword.get(opts, :timeout, 5_000)
  poll_interval = 100

  wait_until(timeout, poll_interval, fn ->
    check_email_arrived(email)
  end)
end

Debugging Tips

  1. Leverage Enhanced Error Messages: The framework now provides clickable file:line references in error messages that take you directly to the failing scenario
  2. Review Step Execution History: Error messages include a visual history (✓ for passed, ✗ for failed) showing which steps executed before the failure
  3. Use IO.inspect in steps: Temporarily add IO.inspect(context) to see the current state
  4. Run single scenarios: Focus on one test at a time during debugging
  5. Use meaningful assertions: Include context in assertion messages
  6. Take advantage of formatted HTML output: When debugging PhoenixTest failures, the error messages now display HTML elements with proper indentation
step "the order should be completed", context do
  order = context.order
  assert order.status == "completed",
         "Expected order #{order.id} to be completed, but was #{order.status}"
  context
end

When an error occurs, you'll see output like:

** (Cucumber.StepError) Step failed:

  Then the order should be completed

in scenario "Order Processing" at test/features/orders.feature:25
matching pattern: "the order should be completed"

Expected order 12345 to be completed, but was pending

Step execution history:
   Given I have items in my cart
   When I submit the order
   Then the order should be completed

The file reference test/features/orders.feature:25 is clickable in most editors and terminals.