Dmitrii Korolkov

github | email

How to Use Cucumber for Python (Behave)

Cucumber is a powerful tool for Behavior-Driven Development (BDD), enabling teams to write tests in a natural, human-readable format. In Python, the most popular implementation of Cucumber is Behave. Using Gherkin syntax and the Given-When-Then style, Behave helps QA engineers, developers, and product teams collaborate effectively while keeping tests clean, readable, and scalable.


1. Installing and Setting Up Behave


pip install behave
                    

Create the recommended project structure:


project/
└── features/
    ├── steps/
    ├── environment.py
    └── example.feature
                    

2. Writing Your First Feature File


# example.feature
Feature: Basic math operations
As a user
I want to perform simple math
So that I can see correct results

Scenario: Adding two numbers
    Given I have two numbers 3 and 5
    When I add them
    Then the result should be 8
                    

3. Implementing Step Definitions


# steps/math_steps.py
from behave import given, when, then

@given("I have two numbers 3 and 5")
def step_given_two_numbers(context):
    context.a = 3
    context.b = 5

@when("I add them")
def step_when_add(context):
    context.result = context.a + context.b

@then("the result should be 8")
def step_then_result(context):
    assert context.result == 8
                    

4. Scenario Outlines with Examples


Scenario Outline: Multiplying numbers
Given I have numbers <a> and <b>
When I multiply them
Then the result should be <result>

Examples:
| a | b | result |
| 2 | 3 | 6      |
| 4 | 5 | 20     |
                    

@given('I have numbers {a:d} and {b:d}')
def step_given_numbers(context, a, b):
    context.a = a
    context.b = b

@when("I multiply them")
def step_multiply(context):
    context.result = context.a * context.b

@then("the result should be {result:d}")
def step_then_check(context, result):
    assert context.result == result
                    

5. Hooks and Environment Setup


# environment.py
def before_all(context):
    print("Test suite is starting...")

def before_scenario(context, scenario):
    print(f"Starting scenario: {scenario.name}")

def after_scenario(context, scenario):
    print(f"Finished scenario: {scenario.name}")

def after_all(context):
    print("All tests completed.")
                    

6. Using Background for Shared Setup

The Background section allows you to define steps that run before each scenario in a feature. It is ideal for repetitive setup like opening pages, setting initial states, or preparing test data.


# example.feature
Feature: User login

Background:
    Given I am on the login page

Scenario: Successful login
    When I enter valid credentials
    Then I should see the dashboard

Scenario: Failed login
    When I enter invalid credentials
    Then I should see an error message
                    

@given("I am on the login page")
def step_open_login(context):
    context.page = "login"

@when("I enter valid credentials")
def step_valid_credentials(context):
    context.result = "dashboard"

@when("I enter invalid credentials")
def step_invalid_credentials(context):
    context.result = "error"

@then("I should see the dashboard")
def step_dashboard(context):
    assert context.result == "dashboard"

@then("I should see an error message")
def step_error(context):
    assert context.result == "error"
                    

7. Custom Parameter Types

Behave allows you to define custom parameter converters. This improves readability and eliminates manual parsing inside step functions.


# steps/custom_types.py
from behave import register_type
import parse

def parse_float(text):
    return float(text)

register_type(Float=parse_float)
                    

Usage inside a step:


@when("I divide {a:Float} by {b:Float}")
def step_divide(context, a, b):
    context.result = a / b
                    

8. Reusing Steps Across Features

Behave loads all step definitions from the steps/ directory, which makes it easy to create shared step libraries. This prevents duplication and keeps tests maintainable.


features/
└── steps/
    ├── common_steps.py
    ├── login_steps.py
    ├── api_steps.py
    └── math_steps.py
                    

# steps/common_steps.py
from behave import given

@given("the system is initialized")
def step_system_initialized(context):
    context.initialized = True
                    

Scenario: System check
Given the system is initialized
When I run diagnostics
Then it should succeed
                    

9. Working with Data Tables

Data tables provide a clean way to pass multiple rows of structured data into steps. This is ideal for lists of users, test datasets, or bulk actions.


Scenario: Creating multiple users
Given the following users exist:
    | name  | role    |
    | John  | admin   |
    | Alice | editor  |
    | Bob   | viewer  |
                    

@given("the following users exist:")
def step_users_exist(context):
    context.users = []
    for row in context.table:
        context.users.append({
            "name": row["name"],
            "role": row["role"]
        })
                    

10. Running Behave Tests


behave
                    

behave features/example.feature
                    

behave --tags=@smoke
                    

11. Best Practices

  • Write feature files in business language.
  • Keep step definitions short and reusable.
  • Put complex logic inside helper modules.
  • Use Background only when absolutely needed.
  • Organize steps into logical modules.
  • Use custom types to avoid manual parsing.