← All skills

Behave Skill

Bdd testingPython

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill behave-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

Behave — Advanced Playbook

§1 Project Setup & Configuration

Installation

pip install behave
pip install behave-html-formatter     # HTML reports
pip install selenium                  # Web UI testing
pip install requests                  # API testing
pip install allure-behave             # Allure reports
pip install PyHamcrest                # Better assertions

Project Structure

features/
├── login.feature
├── checkout.feature
├── api/
│   ├── users.feature
│   └── products.feature
├── steps/
│   ├── login_steps.py
│   ├── checkout_steps.py
│   ├── api_steps.py
│   └── common_steps.py
├── pages/
│   ├── base_page.py
│   ├── login_page.py
│   └── dashboard_page.py
├── environment.py
├── behave.ini
└── fixtures.py

behave.ini

[behave]
paths = features
format = pretty
         json
         html
outfiles =
    reports/results.json
    reports/report.html
color = true
show_timings = true
junit = true
junit_directory = reports/junit
log_capture = true
logging_level = INFO
default_tags = ~@wip ~@skip

setup.cfg (Alternative Config)

[behave]
paths = features
format = progress3
junit = true
junit_directory = reports/junit
show_timings = true

§2 Feature Files — Gherkin Patterns

UI Feature with Scenario Outline

# features/login.feature
@ui @smoke
Feature: User Login
  As a registered user
  I want to log in to my account
  So that I can access my dashboard

  Background:
    Given the test database is ready
    And I am on the login page

  @critical
  Scenario: Successful login
    When I enter "user@test.com" as email
    And I enter "ValidPass123" as password
    And I click the login button
    Then I should be redirected to the dashboard
    And I should see "Welcome back, Test User"

  @negative
  Scenario Outline: Login with invalid credentials
    When I enter "<email>" as email
    And I enter "<password>" as password
    And I click the login button
    Then I should see error message "<error>"

    Examples: Invalid credentials
      | email            | password     | error                      |
      | wrong@test.com   | ValidPass123 | Invalid email or password  |
      | user@test.com    | wrong        | Invalid email or password  |
      |                  | ValidPass123 | Email is required          |
      | user@test.com    |              | Password is required       |

API Feature with Data Tables

# features/api/users.feature
@api
Feature: Users API
  As an API consumer
  I want to manage users via REST API

  Background:
    Given I am authenticated as admin

  Scenario: Create a new user
    When I send a POST request to "/api/users" with:
      | name  | Test User              |
      | email | newuser@test.com       |
      | role  | viewer                 |
    Then the response status should be 201
    And the response should contain "id"
    And the response field "name" should be "Test User"

  Scenario: List users with pagination
    When I send a GET request to "/api/users?page=1&limit=10"
    Then the response status should be 200
    And the response should contain at most 10 items

  Scenario: Delete user returns 204
    Given a user exists with email "delete-me@test.com"
    When I send a DELETE request to the user endpoint
    Then the response status should be 204

§3 Step Definitions — Production Patterns

Common Steps with Type Registration

# features/steps/common_steps.py
from behave import given, when, then, register_type, step
from hamcrest import assert_that, equal_to, contains_string, greater_than
import parse


def parse_boolean(text):
    return text.strip().lower() in ('true', 'yes', '1')


register_type(Boolean=parse_boolean)


@given('the test database is ready')
def step_setup_db(context):
    context.db_helper.reset_and_seed()


@given('I am on the "{page_name}" page')
def step_navigate(context, page_name):
    pages = {
        'login': '/login',
        'register': '/register',
        'dashboard': '/dashboard',
        'products': '/products',
    }
    url = pages.get(page_name, f'/{page_name}')
    context.browser.get(f"{context.base_url}{url}")


@then('I should see "{text}"')
def step_see_text(context, text):
    assert_that(context.browser.page_source, contains_string(text))


@then('I should see error message "{message}"')
def step_see_error(context, message):
    error_el = context.browser.find_element("css selector", ".error-message")
    assert_that(error_el.text, contains_string(message))

API Steps

# features/steps/api_steps.py
from behave import given, when, then
from hamcrest import assert_that, equal_to, has_key, has_length, less_than_or_equal_to
import requests
import json


@given('I am authenticated as {role}')
def step_auth(context, role):
    credentials = {
        'admin': ('admin@test.com', 'AdminPass123'),
        'user': ('user@test.com', 'UserPass123'),
    }
    email, password = credentials[role]
    response = requests.post(f"{context.api_url}/auth/login", json={
        'email': email, 'password': password,
    })
    context.auth_token = response.json()['token']
    context.auth_headers = {
        'Authorization': f'Bearer {context.auth_token}',
        'Content-Type': 'application/json',
    }


@given('a user exists with email "{email}"')
def step_create_user(context, email):
    response = requests.post(f"{context.api_url}/users",
        json={'name': 'Temp User', 'email': email, 'role': 'viewer'},
        headers=context.auth_headers)
    context.created_user_id = response.json()['id']
    context.created_user_url = f"/api/users/{context.created_user_id}"


@when('I send a {method} request to "{url}"')
def step_send_request(context, method, url):
    full_url = f"{context.api_url}{url.replace('/api', '')}"
    context.response = requests.request(
        method, full_url, headers=context.auth_headers)


@when('I send a {method} request to "{url}" with')
def step_send_request_with_table(context, method, url):
    data = dict(context.table.rows)
    full_url = f"{context.api_url}{url.replace('/api', '')}"
    context.response = requests.request(
        method, full_url, json=data, headers=context.auth_headers)


@when('I send a DELETE request to the user endpoint')
def step_delete_user(context):
    url = f"{context.api_url}/users/{context.created_user_id}"
    context.response = requests.delete(url, headers=context.auth_headers)


@then('the response status should be {status:d}')
def step_check_status(context, status):
    assert_that(context.response.status_code, equal_to(status))


@then('the response should contain "{field}"')
def step_response_has_field(context, field):
    body = context.response.json()
    assert_that(body, has_key(field))


@then('the response field "{field}" should be "{value}"')
def step_response_field_value(context, field, value):
    body = context.response.json()
    assert_that(str(body[field]), equal_to(value))


@then('the response should contain at most {count:d} items')
def step_response_count(context, count):
    body = context.response.json()
    data = body.get('data', body)
    assert_that(data, has_length(less_than_or_equal_to(count)))

§4 Environment Hooks — Lifecycle

# features/environment.py
import os
import logging
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service

logger = logging.getLogger('behave')


def before_all(context):
    """Global setup — runs once before everything."""
    context.base_url = os.environ.get('BASE_URL', 'http://localhost:3000')
    context.api_url = os.environ.get('API_URL', 'http://localhost:3000/api')
    context.screenshots_dir = 'reports/screenshots'
    os.makedirs(context.screenshots_dir, exist_ok=True)

    # Database helper
    from features.fixtures import DatabaseHelper
    context.db_helper = DatabaseHelper()


def before_feature(context, feature):
    """Runs before each feature file."""
    logger.info(f"Starting feature: {feature.name}")


def before_scenario(context, scenario):
    """Runs before each scenario."""
    # Start browser for UI tests
    if 'ui' in scenario.effective_tags:
        chrome_options = Options()
        chrome_options.add_argument('--headless')
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        chrome_options.add_argument('--window-size=1920,1080')

        remote_url = os.environ.get('SELENIUM_URL')
        if remote_url:
            context.browser = webdriver.Remote(
                command_executor=remote_url,
                options=chrome_options)
        else:
            context.browser = webdriver.Chrome(options=chrome_options)

        context.browser.implicitly_wait(10)

    # Database transaction for isolation
    context.db_helper.begin_transaction()


def after_scenario(context, scenario):
    """Runs after each scenario."""
    # Screenshot on failure
    if scenario.status == 'failed' and hasattr(context, 'browser'):
        safe_name = scenario.name.replace(' ', '_')[:50]
        filepath = f"{context.screenshots_dir}/{safe_name}.png"
        context.browser.save_screenshot(filepath)
        logger.error(f"Screenshot saved: {filepath}")

    # Close browser
    if hasattr(context, 'browser'):
        context.browser.quit()
        del context.browser

    # Rollback database
    context.db_helper.rollback()


def after_all(context):
    """Global teardown."""
    context.db_helper.close()

§5 Page Objects

Base Page

# features/pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By


class BasePage:
    def __init__(self, browser, base_url):
        self.browser = browser
        self.base_url = base_url
        self.wait = WebDriverWait(browser, 15)

    def navigate(self, path=''):
        self.browser.get(f"{self.base_url}{path}")

    def find(self, by, value):
        return self.wait.until(EC.visibility_of_element_located((by, value)))

    def click(self, by, value):
        self.wait.until(EC.element_to_be_clickable((by, value))).click()

    def type_text(self, by, value, text):
        element = self.find(by, value)
        element.clear()
        element.send_keys(text)

    def get_text(self, by, value):
        return self.find(by, value).text


class LoginPage(BasePage):
    URL = '/login'
    EMAIL_INPUT = (By.ID, 'email')
    PASSWORD_INPUT = (By.ID, 'password')
    SUBMIT_BTN = (By.ID, 'login-submit')
    ERROR_MSG = (By.CSS_SELECTOR, '.error-message')

    def open(self):
        self.navigate(self.URL)

    def login(self, email, password):
        self.type_text(*self.EMAIL_INPUT, email)
        self.type_text(*self.PASSWORD_INPUT, password)
        self.click(*self.SUBMIT_BTN)

    def get_error(self):
        return self.get_text(*self.ERROR_MSG)

§6 Fixtures & Test Data

# features/fixtures.py
import os
import json
import psycopg2
from contextlib import contextmanager


class DatabaseHelper:
    def __init__(self):
        self.conn = psycopg2.connect(os.environ.get(
            'DATABASE_URL', 'postgresql://test:test@localhost/testdb'))
        self.conn.autocommit = False

    def begin_transaction(self):
        self.conn.rollback()  # Clear any pending

    def rollback(self):
        self.conn.rollback()

    def reset_and_seed(self):
        with self.conn.cursor() as cur:
            cur.execute("DELETE FROM orders")
            cur.execute("DELETE FROM cart_items")
            cur.execute("DELETE FROM users WHERE email LIKE '%@test.com'")
            cur.execute("""
                INSERT INTO users (name, email, password_hash, role)
                VALUES
                    ('Admin', 'admin@test.com', '$hash', 'admin'),
                    ('Test User', 'user@test.com', '$hash', 'user')
            """)
        self.conn.commit()

    def close(self):
        self.conn.close()


class TestDataLoader:
    @staticmethod
    def load_json(filename):
        path = os.path.join('features', 'data', filename)
        with open(path) as f:
            return json.load(f)

    @staticmethod
    def load_users():
        return TestDataLoader.load_json('test_users.json')

§7 LambdaTest Integration

# features/environment.py — LambdaTest section
def _create_lambdatest_browser(context, scenario):
    """Create browser on LambdaTest cloud."""
    lt_options = {
        'name': scenario.name,
        'build': os.environ.get('BUILD_NAME', 'Behave-Build'),
        'project': 'MyProject',
        'platformName': 'Windows 11',
        'w3c': True,
        'video': True,
        'network': True,
        'console': True,
    }
    chrome_options = Options()
    chrome_options.set_capability('LT:Options', lt_options)

    username = os.environ['LT_USERNAME']
    access_key = os.environ['LT_ACCESS_KEY']
    hub_url = f'https://{username}:{access_key}@hub.lambdatest.com/wd/hub'

    context.browser = webdriver.Remote(
        command_executor=hub_url, options=chrome_options)

§8 CI/CD Integration

GitHub Actions

name: Behave Tests
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  behave:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports: ['5432:5432']
        options: --health-cmd pg_isready --health-interval 10s
      selenium:
        image: selenium/standalone-chrome:latest
        ports: ['4444:4444']

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'

      - run: pip install behave selenium requests PyHamcrest allure-behave behave-html-formatter

      - name: Run Behave Tests
        run: |
          behave \
            --format=pretty \
            --format=allure_behave.formatter:AllureFormatter \
            --outfile=reports/allure \
            --junit \
            --junit-directory=reports/junit \
            --tags="~@wip" \
            --no-capture
        env:
          BASE_URL: http://localhost:3000
          API_URL: http://localhost:3000/api
          SELENIUM_URL: http://localhost:4444/wd/hub
          DATABASE_URL: postgresql://test:test@localhost:5432/testdb

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: behave-results
          path: reports/

§9 Debugging Table

#ProblemCauseFix
1NotImplementedError for stepStep definition not foundRun behave --snippets-type=parse to generate stubs
2context.browser not availableMissing @ui tag on scenarioAdd @ui tag; check before_scenario creates browser for tag
3StaleElementReferenceExceptionPage reloaded after finding elementRe-find element after page changes; use explicit waits
4Tests pass alone, fail togetherShared state via context leakingReset context attributes in before_scenario; use transaction rollback
5Scenario Outline shows wrong dataMismatch between placeholder and Examples columnColumn names must match <placeholder> names exactly
6before_scenario not firingWrong function signatureMust be def before_scenario(context, scenario): with both args
7Screenshots not savedDirectory doesn't exist or wrong pathUse os.makedirs(dir, exist_ok=True) in before_all
8context.table is NoneStep pattern doesn't expect table dataUse table in step text: When I send a POST with: (colon triggers table)
9Allure report emptyFormatter not installed or wrong output pathVerify allure-behave installed; check --outfile path
10CI fails with selenium.common.exceptionsChrome/ChromeDriver version mismatchUse selenium/standalone-chrome:latest Docker image
11Tags not filteringWrong tag syntax in behave.iniUse --tags="@smoke" not --tags="smoke" (include @)
12Import errors in stepsStep files not in features/steps/ directoryAll step files must be in features/steps/ subdirectory

§10 Best Practices Checklist

  1. Use Background for shared Given steps — avoid repetition across scenarios
  2. Use Scenario Outline with Examples — parameterize without duplicating
  3. Use context for sharing state — never use global variables
  4. Use environment.py hooks — setup/teardown at correct scope
  5. Use transaction rollback — fastest isolation between scenarios
  6. Use Page Objects — abstract browser interaction from step definitions
  7. Use register_type() for custom step parsers — {count:d}, {Boolean}
  8. Use PyHamcrest matchers — readable assertions: assert_that(x, equal_to(y))
  9. Use tags for organization — @smoke, @api, @ui, @wip, @slow
  10. Use --tags="~@wip" in CI — exclude incomplete scenarios
  11. Use screenshot on failure — attach to Allure or HTML reports
  12. Use JUnit output for CI — standard format for test report publishing
  13. Keep step definitions reusable — one step per action, not per scenario
  14. Write features in business language — domain experts should review Gherkin