Integration examples

<- Back to home

This page focuses on copy-paste patterns for real Behave projects. Start with the smallest recipe that solves your problem, then add the optional layers only when they earn their place.

Recipe 1: the smallest useful integration

Use this when you only want scoped object creation and cleanup.

features/
  behave-toolkit.yaml
  environment.py
  steps/
    reporting_steps.py
  smoke.feature

features/behave-toolkit.yaml:

version: 1
variables:
  report_name: report.json

objects:
  workspace:
    factory: tempfile.TemporaryDirectory
    scope: feature
    cleanup: cleanup

  workspace_path:
    factory: pathlib.Path
    scope: feature
    args:
      - $ref: workspace
        attr: name

  report_path:
    factory: pathlib.Path
    scope: scenario
    args:
      - $ref: workspace_path
      - $var: report_name

features/environment.py:

from pathlib import Path

from behave_toolkit import (
    activate_feature_scope,
    activate_scenario_scope,
    install,
)

CONFIG_PATH = Path(__file__).with_name("behave-toolkit.yaml")


def before_all(context):
    install(context, CONFIG_PATH)


def before_feature(context, feature):
    del feature
    activate_feature_scope(context)


def before_scenario(context, scenario):
    del scenario
    activate_scenario_scope(context)

features/steps/reporting_steps.py:

from behave import then


@then("the report file can be written")
def step_report_file_can_be_written(context):
    context.report_path.write_text("ok\n", encoding="utf-8")
    assert context.report_path.exists()

Recipe 2: split config into a directory

Use this when one YAML file starts to feel crowded or when different people own different areas.

features/
  behave-toolkit/
    00-variables.yaml
    10-parsers.yaml
    20-objects.yaml
  environment.py
  support_types.py

features/behave-toolkit/00-variables.yaml:

version: 1
variables:
  environment_name: qa
  base_url: https://qa.example.test
  report_name: latest-report.json

features/behave-toolkit/10-parsers.yaml:

parsers:
  step_matcher: cfparse
  types:
    Status:
      enum: support_types.Status
      case_sensitive: false

features/behave-toolkit/20-objects.yaml:

objects:
  artifacts_path:
    factory: pathlib.Path
    scope: global
    args:
      - artifacts

  test_log_path:
    factory: pathlib.Path
    scope: global
    args:
      - $ref: artifacts_path
      - test-run.log

  workspace:
    factory: tempfile.TemporaryDirectory
    scope: feature
    cleanup: cleanup

  workspace_path:
    factory: pathlib.Path
    scope: feature
    args:
      - $ref: workspace
        attr: name

  report_path:
    factory: pathlib.Path
    scope: scenario
    args:
      - $ref: workspace_path
      - $var: report_name

features/support_types.py:

from enum import Enum


class Status(Enum):
    ACTIVE = "active"
    PENDING = "pending"

Recipe 3: one environment.py that combines the common optional helpers

Use this when you want parser helpers, feature-file variables, scenario cycling, and one persistent log together.

from pathlib import Path

from behave_toolkit import (
    activate_feature_scope,
    activate_scenario_scope,
    configure_parsers,
    configure_test_logging,
    expand_scenario_cycles,
    format_cycle_progress,
    install,
    substitute_feature_variables,
)

CONFIG_PATH = Path(__file__).with_name("behave-toolkit")

# Import-time setup for Behave custom types.
configure_parsers(CONFIG_PATH)


def before_all(context):
    install(context, CONFIG_PATH)
    substitute_feature_variables(context)
    added = expand_scenario_cycles(context)
    context.test_logger = configure_test_logging(context.test_log_path)
    if added:
        context.test_logger.info("Expanded %s extra cycle runs", added)


def before_feature(context, feature):
    del feature
    activate_feature_scope(context)


def before_scenario(context, scenario):
    activate_scenario_scope(context)
    progress = format_cycle_progress(scenario)
    if progress is None:
        context.test_logger.info("%s", scenario.name)
    else:
        context.test_logger.info("Cycle %s -> %s", progress, scenario.name)

The ordering above is the recommended one when you combine these helpers:

  1. configure_parsers(CONFIG_PATH) at import time

  2. install(context, CONFIG_PATH) in before_all()

  3. substitute_feature_variables(context) if you use {{var:name}}

  4. expand_scenario_cycles(context) if you use @cycling(N)

  5. configure_test_logging(...) or configure_loggers(context) after global objects exist

Recipe 4: reuse root variables directly in feature files

features/login.feature:

Feature: Login against {{var:environment_name}}

  Scenario: Open the configured home page
    Given I open {{var:base_url}}

features/environment.py:

from pathlib import Path

from behave_toolkit import install, substitute_feature_variables

CONFIG_PATH = Path(__file__).with_name("behave-toolkit")


def before_all(context):
    install(context, CONFIG_PATH)
    substitute_feature_variables(context)

Feature placeholders use {{var:name}}. They are resolved from the root variables: section and apply to names, descriptions, step text, docstrings, and tables. Tags are intentionally left unchanged.

Recipe 5: upgrade from one log file to named loggers in YAML

Use this when one persistent test-run.log is no longer enough.

Add paths in objects::

objects:
  artifacts_path:
    factory: pathlib.Path
    scope: global
    args:
      - artifacts

  test_log_path:
    factory: pathlib.Path
    scope: global
    args:
      - $ref: artifacts_path
      - test-run.log

  diagnostics_log_path:
    factory: pathlib.Path
    scope: global
    args:
      - $ref: artifacts_path
      - diagnostics.log

Add named logger definitions:

logging:
  test_run:
    path:
      $ref: test_log_path
    logger_name: suite-tests
    inject_as: test_logger

  diagnostics:
    path:
      $ref: diagnostics_log_path
    logger_name: suite-diagnostics
    inject_as: diagnostics_logger
    console: false

Wire them from before_all():

from pathlib import Path

from behave_toolkit import configure_loggers, install

CONFIG_PATH = Path(__file__).with_name("behave-toolkit")


def before_all(context):
    install(context, CONFIG_PATH)
    configure_loggers(context)

Use YAML-defined loggers only when you really want multiple named outputs. For many suites, the smaller configure_test_logging() helper stays the better default.

Recipe 6: generate a step reference site for a downstream suite

Once your step library grows, generate a searchable reference site directly from your Behave project:

behave-toolkit-docs --features-dir features --output-dir docs/behave-toolkit --config-path features/behave-toolkit
python -m sphinx -b html docs/behave-toolkit docs/_build/behave-toolkit

Pass --config-path when your feature files use {{var:name}}, so example matching sees the substituted text.

Which recipe should you start with?

  • Start with Recipe 1 if you only need scoped objects and cleanup.

  • Add Recipe 2 when config ownership grows and one YAML file stops scaling.

  • Use Recipe 3 if your suite already needs several optional helpers together.

  • Use Recipe 4 only when feature-file placeholders make scenarios easier to read or easier to share across environments.

  • Use Recipe 5 only when one persistent suite log is no longer enough.

  • Use Recipe 6 once your step library has become large enough that discovery is a real problem.