docs/best_practices.md

# Best Practices for Cucumber Tests

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:
```gherkin
Scenario: User interaction
  Given a user
  When the user does stuff
  Then the outcome is good
```

Good:
```gherkin
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:

```elixir
# 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:

```elixir
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:

```elixir
# 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:

```elixir
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:

```elixir
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:

```gherkin
@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:
```bash
mix test --only authentication
mix test --exclude wip
```

### Background vs. Helper Steps

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

```gherkin
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:

```elixir
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

```elixir
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.