Skip to content

Chapter II - Writing Feature Files

(avr. time for this chapter: 1 day)

This chapter covers advanced Gherkin patterns, best practices for writing maintainable feature files, and common anti-patterns to avoid.

Feature File Best Practices

Feature Description

Write clear, business-focused feature descriptions:

# Good - Clear business value
Feature: Password Reset
  As a forgetful user
  I want to reset my password via email
  So that I can regain access to my account

# Bad - Too technical
Feature: Password Reset
  As a user
  I want to call the password reset API
  So that a token is generated in the database

Scenario Naming

Use descriptive, behavior-focused names:

# Good - Describes behavior
Scenario: User receives confirmation email after successful registration
Scenario: Cart total updates when item quantity changes
Scenario: Search returns no results for non-existent product

# Bad - Vague or technical
Scenario: Test registration
Scenario: Cart test 1
Scenario: API returns 404

Step Writing Guidelines

Write steps that are: - Declarative (what, not how) - Business-focused (user perspective) - Reusable (avoid specific values in step text)

# Good - Declarative
Given I am logged in as a premium user
When I add a product to my wishlist
Then I should see the product in my wishlist

# Bad - Imperative (too detailed)
Given I navigate to "https://example.com/login"
And I enter "user@example.com" in the email field
And I enter "password123" in the password field
And I click the button with id "login-btn"
When I click on the heart icon next to product id 123
Then I should see product 123 in the div with class "wishlist-items"

Scenario Outline Patterns

Basic Scenario Outline

Scenario Outline: Login with different user types
  Given I am on the login page
  When I login as a "<user_type>" user
  Then I should see the "<dashboard>" dashboard

  Examples:
    | user_type | dashboard     |
    | admin     | Admin         |
    | manager   | Management    |
    | employee  | Employee      |
    | guest     | Limited       |

Multiple Example Tables

Scenario Outline: Shipping cost calculation
  Given I have items worth $<subtotal> in my cart
  When I select "<shipping>" shipping to "<region>"
  Then my shipping cost should be $<cost>

  @domestic
  Examples: Domestic Shipping
    | subtotal | shipping | region    | cost  |
    | 50       | standard | East      | 5.99  |
    | 50       | express  | East      | 12.99 |
    | 100      | standard | East      | 0.00  |

  @international
  Examples: International Shipping
    | subtotal | shipping | region    | cost  |
    | 50       | standard | Europe    | 15.99 |
    | 50       | express  | Europe    | 29.99 |
    | 100      | standard | Europe    | 10.99 |

Complex Data in Outlines

Scenario Outline: Form validation messages
  Given I am on the registration form
  When I submit with <field> as "<value>"
  Then I should see the error "<error_message>"

  Examples:
    | field    | value              | error_message                    |
    | email    |                    | Email is required                |
    | email    | invalid            | Please enter a valid email       |
    | email    | test@              | Please enter a valid email       |
    | password |                    | Password is required             |
    | password | 123                | Password must be at least 8 chars|
    | password | password           | Password must contain a number   |

Background Best Practices

When to Use Background

Use Background for: - Common preconditions shared by ALL scenarios - Setup that doesn't change between scenarios

Feature: User Profile Management

  Background:
    Given I am logged in as "john@example.com"
    And I am on my profile page

  Scenario: Update display name
    When I change my display name to "John Smith"
    Then my display name should be "John Smith"

  Scenario: Update email preferences
    When I disable marketing emails
    Then I should not receive marketing emails

When NOT to Use Background

Don't use Background when: - Only some scenarios need the setup - The setup is complex and obscures the scenario

# Bad - Background doesn't apply to all scenarios
Feature: Shopping Cart

  Background:
    Given I am logged in
    And I have 3 items in my cart

  Scenario: Add item to cart
    # This scenario doesn't need items in cart!
    When I add a product to my cart
    Then my cart should have 4 items

  Scenario: Empty cart message
    # This scenario needs an EMPTY cart!
    Given my cart is empty  # Contradicts background!
    Then I should see "Your cart is empty"

Hooks and World

Cucumber Hooks

// support/hooks.ts
import { Before, After, BeforeAll, AfterAll, BeforeStep, AfterStep } from '@cucumber/cucumber';

BeforeAll(async function () {
  // Runs once before all scenarios
  console.log('Starting test suite');
});

AfterAll(async function () {
  // Runs once after all scenarios
  console.log('Test suite complete');
});

Before(async function () {
  // Runs before each scenario
  console.log('Starting scenario:', this.pickle.name);
});

After(async function (scenario) {
  // Runs after each scenario
  if (scenario.result?.status === 'FAILED') {
    // Take screenshot on failure
    console.log('Scenario failed:', scenario.pickle.name);
  }
});

// Hooks with tags
Before({ tags: '@database' }, async function () {
  // Only runs for scenarios tagged @database
  await this.seedDatabase();
});

After({ tags: '@database' }, async function () {
  await this.cleanDatabase();
});

World Object

The World provides shared context between steps:

// support/world.ts
import { setWorldConstructor, World, IWorldOptions } from '@cucumber/cucumber';

interface CustomWorld extends World {
  currentUser: { email: string; role: string } | null;
  cart: { items: any[]; total: number };
  lastResponse: any;
}

class TestWorld extends World implements CustomWorld {
  currentUser = null;
  cart = { items: [], total: 0 };
  lastResponse = null;

  constructor(options: IWorldOptions) {
    super(options);
  }

  async login(email: string, password: string) {
    // Shared login logic
    this.currentUser = { email, role: 'user' };
  }

  async addToCart(product: any) {
    this.cart.items.push(product);
    this.cart.total += product.price;
  }
}

setWorldConstructor(TestWorld);

Using World in Steps

import { Given, When, Then } from '@cucumber/cucumber';
import { CustomWorld } from '../support/world';

Given('I am logged in as {string}', async function (this: CustomWorld, email: string) {
  await this.login(email, 'password123');
});

When('I add {string} to my cart', async function (this: CustomWorld, productName: string) {
  const product = { name: productName, price: 29.99 };
  await this.addToCart(product);
});

Then('my cart total should be {float}', function (this: CustomWorld, expectedTotal: number) {
  expect(this.cart.total).to.equal(expectedTotal);
});

Parameter Types

Built-in Parameter Types

// {int} - Integer
When('I have {int} items', function (count: number) {});

// {float} - Decimal number
Then('the total is {float}', function (total: number) {});

// {string} - Quoted string
When('I search for {string}', function (query: string) {});

// {word} - Single word without quotes
Given('I am a {word} user', function (role: string) {});

Custom Parameter Types

// support/parameter-types.ts
import { defineParameterType } from '@cucumber/cucumber';

// Custom type for user roles
defineParameterType({
  name: 'role',
  regexp: /admin|manager|employee|guest/,
  transformer: (role) => role
});

// Custom type for currency
defineParameterType({
  name: 'currency',
  regexp: /\$[\d,]+\.?\d*/,
  transformer: (amount) => parseFloat(amount.replace(/[$,]/g, ''))
});

// Custom type for dates
defineParameterType({
  name: 'date',
  regexp: /\d{4}-\d{2}-\d{2}/,
  transformer: (dateStr) => new Date(dateStr)
});

Using Custom Types

Scenario: Role-based access
  Given I am logged in as an admin user
  When I access the admin panel
  Then I should see the admin dashboard

Scenario: Price calculation
  Given the product costs $99.99
  When I apply a 10% discount
  Then the final price should be $89.99

Scenario: Scheduled task
  Given I schedule a task for 2024-03-15
  Then the task should appear on 2024-03-15
Given('I am logged in as a/an {role} user', function (role: string) {
  this.currentUser = { role };
});

Given('the product costs {currency}', function (price: number) {
  this.product = { price };
});

Given('I schedule a task for {date}', function (date: Date) {
  this.scheduledDate = date;
});

Anti-Patterns to Avoid

1. Incidental Details

# Bad - Too many irrelevant details
Scenario: User login
  Given I open Chrome browser
  And I navigate to "https://example.com"
  And I wait for the page to load
  And I see the login form
  When I click on the email input field
  And I type "test@example.com"
  And I click on the password input field
  And I type "password123"
  And I click the blue login button
  Then I see the URL change to "/dashboard"

# Good - Focus on behavior
Scenario: User login
  Given I am on the login page
  When I login with valid credentials
  Then I should be on the dashboard

2. Conjunction Steps

# Bad - Multiple actions in one step
When I login and add a product to cart and proceed to checkout

# Good - Separate steps
When I login with valid credentials
And I add "Laptop" to my cart
And I proceed to checkout

3. Scenario Dependency

# Bad - Scenarios depend on each other
Scenario: Create user
  When I create user "john@example.com"
  Then the user should be created

Scenario: Login with created user
  # Depends on previous scenario!
  When I login as "john@example.com"
  Then I should be logged in

# Good - Independent scenarios
Scenario: Create user
  When I create user "john@example.com"
  Then the user should be created

Scenario: Login with existing user
  Given a user "john@example.com" exists
  When I login as "john@example.com"
  Then I should be logged in

4. Feature Envy

# Bad - Testing implementation details
Scenario: User registration
  When I submit the registration form
  Then the users table should have 1 new row
  And the email_queue table should have 1 new row
  And the audit_log should contain "USER_CREATED"

# Good - Testing behavior
Scenario: User registration
  When I submit the registration form
  Then I should receive a confirmation email
  And I should be able to login with my new account

Exercise: Refactor Feature Files

Exercise 1: Refactor Bad Feature

Refactor this poorly written feature:

Feature: test shopping

Scenario: test 1
  Given I open browser
  And go to http://localhost:3000
  And click login
  And type admin@test.com in email
  And type 123456 in password
  And click submit
  And wait 2 seconds
  When click on product 1
  And click add to cart button
  And click cart icon
  Then see 1 item in cart div

Scenario: test 2
  # Depends on test 1
  When click checkout
  And fill form
  And click pay
  Then see success

Exercise 2: Write Clean Feature

Write a clean, well-structured feature file for: - User account management (update profile, change password, delete account) - Include at least 5 scenarios - Use Background appropriately - Use Scenario Outline where applicable - Add meaningful tags

Self-Assessment

After completing this chapter, you should be able to:

  • [ ] Write clear, business-focused feature descriptions
  • [ ] Use Scenario Outline effectively
  • [ ] Apply Background appropriately
  • [ ] Create and use custom parameter types
  • [ ] Implement hooks and World object
  • [ ] Identify and avoid common anti-patterns

Next Steps

Continue to Chapter III - Step Definitions to learn how to implement step definitions with browser automation.