Behavior Driven Development

Behavior Driven Development (BDD) is an agile software development technique. BDD enables non-technical participants to participate in writing tests.

The HILSTER Testing Framework enables users to write features in a gherkin-like language with add-ons that can be run as tests with seamless integration into the existing ecosystem.

Tests are written in form of Features, Rules, Scenarios and Steps, which are either Given, When, or Then steps. It is also possible to create Data Driven Feature Tests.

Thus every step is implemented in Python based on HILSTER Testing Framework.

HILSTER Testing Framework also offers requirements coverage in combination with the Dashboard.

Features

A feature file describes a feature in natural language with scenarios describing actions and the expected outcome. The filename ends with .feature with UTF-8 encoding. Every feature file must contain one single feature. Every feature can contain one or multiple scenarios or scenario outlines.

A feature may contain further keywords to describe how tests may run.

Lines starting with # are comments.

Scenarios

Scenarios are “tests” which consist of Given, When, and Then statements. They describe preconditions, actions as well as the expected outcomes.

Steps

Given steps specify preconditions, which describe the state of the environment. They are used to put the tested system into an initial state.

When steps specify conditions, for example when car turns right is a condition. They are used to take actions.

Then steps specify assertions and serve for checking if conditions are met. They are used to observe outcomes.

Each statement can be repeated with And as well as But.

HILSTER Testing Framework finds the correct implementation of these steps so that they can be executed as a test.

Writing Features

A feature is written in a file named <feature-name>.feature.

The feature description is written with the Feature: keyword followed by a feature name and an optional indented documentation string in the next lines.

Feature: Example features without info string
Feature: Example features with info string
  This documentation string is optional.

Writing Scenarios

A scenario is described inside a Scenario: keyword. It is indented compared to the feature.

Feature: Heating
  Scenario: Heat up
    Given the refrigeration machine controller is running at 100 rpm
    And the environmental temperature is 21 °C
    When the initial temperature is -100 °C
    Then the refrigeration machine controller does not cool down

Example Feature

The following example contains a very simple feature with a single scenario.

Feature: This is an example feature
    This features is only used as an example

    Scenario: Example
        Given we write an example
        When the interested reader reads it
        Then he will understand how BDD works

Complex Scenarios

As an extension to BDD, scenarios support repeated keywords which allow you to create more complex scenarios.

Feature: This is an example feature
    This features is only used as an example

    Scenario: Complex Example
        Given we write an example
        When the interested reader reads it
        Then he will understand how BDD works

        Given we write another example
        When the interested reader reads it, too
        Then he will understand how BDD works for sure

Implementing Steps

Steps are implemented in Python as methods that are decorated with htf.given, htf.when and htf.then decorators.

The following code shows the implementation of the corresponding steps so that this feature can be run as a test.

@htf.given("we write an example")
def we_write_an_example():
    pass

@htf.when("the interested reader reads it")
def the_interested_reader_reads_it():
    pass

@htf.then("he will understand how BDD works")
def he_will_understand_how_bdd_works():
    pass

The corresponding steps are found via pattern matching. Details are described later.

Steps can be async, too.

@htf.given("we write an example")
async def we_write_an_example():
    pass

@htf.when("the interested reader reads it")
async def the_interested_reader_reads_it():
    pass

@htf.then("he will understand how BDD works")
async def he_will_understand_how_bdd_works():
    pass
htf.given(precondition: str, parser: str | None = None, extra_types: Dict[Any, Any] | None = None, converters: Dict[str, Any] | None = None) Callable[[Callable[[...], Any]], Any]

Decorates a method to make it an implementation of a given step in a behavior driven test. See also https://cucumber.io/docs/gherkin/reference/#given .

Parameters:
  • precondition – gherkin feature precondition

  • parser=None – the parser to parse the parameters from the precondition

  • extra_types=None – extra types for parsing

  • converters=None – optional converters that are applied to parameters after parsing

htf.when(condition: str, parser: str | None = None, extra_types: Dict[Any, Any] | None = None, converters: Dict[str, Any] | None = None) Callable[[Callable[[...], Any]], Any]

Decorates a method to make it an implementation of a when step in a behavior driven test. See also https://cucumber.io/docs/gherkin/reference/#when .

Parameters:
  • condition – gherkin feature condition

  • parser=None – the parser to parse the parameters from the condition

  • extra_types=None – extra types for parsing

  • converters=None – optional converters that are applied to parameters after parsing

htf.then(assertion: str, parser: str | None = None, extra_types: Dict[Any, Any] | None = None, converters: Dict[str, Any] | None = None) Callable[[Callable[[...], Any]], Any]

Decorates a method to make it an implementation of a then step in a behavior driven test. See also https://cucumber.io/docs/gherkin/reference/#then .

Parameters:
  • assertion – gherkin feature assertion

  • parser=None – the parser to parse the parameters from the assertion

  • extra_types=None – extra types for conversion

  • converters=None – optional converters that are applied to parameters after parsing

Running Features

Run htf by giving the specifier for feature file, and also the <step-implementation>.py file. Features and normal tests can also be mixed up.

In Python you can run:

htf.main(tests=["steps.py", "example.feature"])

And on the command-line you can run:

htf steps.py example.feature

Steps with Parameters

You can implement steps with parameters, too. This allows to use one step implementation for more than one step in a feature file.

Feature: Feature with parameters in steps

    Scenario: Monkey eats many Bananas
        Given the monkey is awake
        When the monkey eats 10 bananas
        Then he is not hungry anymore

    Scenario: Monkey eats two Bananas
        Given the monkey is awake
        When the monkey eats 2 bananas
        Then he is still hungry

The When step with the parameter to read the number of bananas is implemented in the following example.

@htf.when("the monkey eats {number_of_bananas} bananas")
def monkey_eats_bananas(number_of_bananas):
    print(f"Monkey eats {number_of_bananas} bananas")

This also works for the other steps.

There are also other parsers for more complex situations.

Long Lines

Long lines can be split up to multiple lines by adding a \ at the end.

Feature: Feature with long lines

    Scenario: Monkey eats many Bananas
        Given the monkey is awake
        When the monkey eats \
             10 bananas
        Then he is not \
             hungry \
             anymore

Multi-Language Support

Features can be written in natural languages and do not need to use the English language. The first line of a feature file controls the language. The default language is English (en).

The following example uses German as the language.

# language: de
Funktion: Heizen
  Szenario: Aufheizen
    Angenommen die Maschine läuft mit 100 Umdrehungen pro Minute
    Wenn die initiale Temperatur 100 °C beträgt
    Dann erreicht die Temperatur der Maschine nicht 120 °C

Context Fixture

If you need to pass data between steps you can use the htf.fixtures.context fixture.

Other fixtures can be used like expected. Step parameters are supplied on the left side.

@htf.when("the monkey eats {number_of_bananas} bananas")
def monkey_eats_bananas(number_of_bananas, context):
    print(f"Monkey eats {number_of_bananas} bananas")
    context.number_of_bananas = int(number_of_bananas)

@htf.then("he is not hungry anymore")
def monkey_is_not_hungry_anymore(context, assertions):
    assertion.assert_greater(context.number_of_bananas, 5, "The monkey did not eat enough bananas!")

@htf.then("he is still hungry")
def monkey_is_still_hungry(context, assertions):
    assertion.assert_less_equal(context.number_of_bananas, 5,
                                "The monkey ate too much bananas! The monkey is dead now.")

To set the actual result of a step, the fixture htf.fixtures.context contains current_step, which is the htf.fixtures.step instance in the current context.

@htf.then("the current number of bananas is {number_of_bananas}")
def check_current_number_of_bananas(number_of_bananas, context):
    current_number_of_bananas = 10
    context.current_step.set_actual_result(current_number_of_bananas)
    htf.assert_almost_equal(current_number_of_bananas, number_of_bananas)

Attach Data to Steps

You can attach more complex data to steps compared to parameters by adding a data table or text in the feature. The data is passed via the htf.fixtures.context fixture as table or text attribute.

Feature: Feature with Data

    Scenario: Monkey feeding
        When the monkey eats bananas
            | i | name   |
            | 1 | first  |
            | 2 | second |
        And the monkey eats bananas and says
            """
            This text is said while eating bananas
            """
            | i | name   |
            | 3 | third  |
            | 4 | fourth |
        Then the monkey says
            """
            Baarrrb.
            """

The implementation of the steps looks like the following

@htf.when("the monkey eats bananas")
def monkey_eats_bananas(context):
    for item in context.table:
        print(f"Eat the {item['i']}th banana called {item['name']}")

@htf.when("the monkey eats bananas and says")
def monkey_eats_bananas(context):
    for item in context.table:
        print(f"Eat the {item['i']}th banana called {item['name']}")
        print(f"Monkey: {context.text}")

@htf.then("the monkey says")
def monkey_says(context):
    print(f"Monkey: {context.text}")

Tagging

HILSTER Testing Framework offers the possibility to add textual tags to features and scenarios as well as scenario outlines.

To do this use the Tags: keyword followed by a list of comma-separated strings.

Feature: Feature with Tags

    Tags: Tag-1, Tag-2, Tag-3

    Scenario: Scenario with Tags
        Tags: Tag-4, Tag-5, Tag-6
        # ...

Tags are combined and put into the test result and thus can be used to filter text by tags as mentioned in Tagging.

Requirements

HILSTER Testing Framework offers the possibility to add textual requirements to features and scenarios as well as scenario outlines.

To do this use the Requirements: keyword followed by a list of comma-separated strings.

Feature: Feature with Requirements

    Requirements: Req-1, Req-2, Req-3

    Scenario: Scenario with Requirements
        Requirements: Req-4, Req-5, Req-6
        # ...

The requirements are combined and put into the test result and thus can be used to perform requirements coverage in combination with the Dashboard.

Implemented Tests

HILSTER Testing Framework offers the possibility to add links to implemented tests to features and scenarios.

To do this use the Implements: keyword followed by a list of comma-separated strings. Each entry can be a url or a simple id.

Feature: Feature with implemented tests

    Implements: Test-1, https://host:port/path/to/Test-2

    Scenario: Scenario with implemented tests
        Implements: Test-3
        # ...

The implemented tests are added to the test report and used to send test results back to ALM/QMS systems.

Background Steps

The keyword Background: can be used when scenarios inside a feature have intersecting preconditions. It contains only Given preconditions that are automatically run in every scenario before running the other steps. It can be used to realize common preconditions.

Feature: Refrigeration machine can preserve internal heat correctly

    Background:
        Given the refrigeration machine controller is running at 100 rpm

    Scenario: Heat up
        Given the environmental temperature is 21 °C
        # ...

    Scenario: Cool down
        Given the environmental temperature is 21 °C
        # ...

Preconditions for Background: are implemented like normal given steps with htf.given.

Rules

The Rule: keyword is optionally used for grouping together related scenarios when they belong to the same feature.

Feature: Monkey movements are correct
  Rule: Monkeys can run if they are healthy
    Scenario: Baboon can run
        Given baboon is healthy and adult
        Then baboon can run
    Scenario: Chimp
        Given chimp is healthy and adult
        Then chimp can run

  Rule: Another rule
     # ...

Scenario Outlines

A scenario outline is a scenario template that dynamically generates scenarios. It is the behavior driven way to realize Data Driven Testing. It consists of steps like a normal scenario that has template variables in it and one data source called Examples. When the scenarios are generated the template variables are replaced automatically.

Feature: Feature with Scenario Outline
  Scenario Outline: Machine does not cool down
    Given the refrigeration machine controller is running at <rpm> rpm
    When the initial temperature is <temperature> °C
    Then the refrigeration machine controller does not cool down

    Examples:
       | rpm | temperature |
       |  78 |         -10 |
       |  80 |          21 |
       |  90 |          25 |
       | 100 |          38 |

Running the examples leads to multiple dynamically generated scenarios with parameters:

Scenario: Machine does not cool down [rpm=78, temperature=-10] (Feature: ...)
Scenario: Machine does not cool down [rpm=80, temperature=21] (Feature: ...)
Scenario: Machine does not cool down [rpm=90, temperature=25] (Feature: ...)
Scenario: Machine does not cool down [rpm=100, temperature=38] (Feature: ...)

The implementation can be done with normal steps with parameters for example.

htf.given("the refrigeration machine controller is running at <rpm> rpm")
def machine_running_at(rpm):
    # this is called once for every line in the data table (78, 80, 90 and 100)

htf.when("the initial temperature is {temperature} °C)
def initial_temperature(temperature):
    # this is called once for every line in the data table (-10, 21, 25 and 38)

Scenario Outline Data Sources

Scenario outlines support multiple different data sources which can be data-tables, external CSV-, JSON, YAML-files and also Python generators. Every data source can have an optional title.

Data-Tables Data Sources

Data-tables are textual tables supplied directly in the feature file. The first line is a header. All other lines are data lines. A data table is parsed into a list of dictionaries with the keys from the header line.

Feature: Feature with Data-Table
    Scenario Outline: Data-Table Scenario
        Given there is a data-table with <key> and <value>

    Examples: Data-Table
        |  key | value |
        |  foo |   bar |
        | ding |  dong |

Running the examples leads to multiple dynamically generated scenarios with parameters:

Scenario: Data-Table Scenario [key=foo, value=bar] (Feature: ...)
Scenario: Data-Table Scenario [key=ding, value=dong] (Feature: ...)

CSV Data Sources

Instead of using data-tables you can also supply external data via a CSV file.

To do this you can supply an example with the CSV: keyword followed by a CSV filename. The filename can be an absolute path or a relative path. Relative paths are relative to the feature file where they appear.

The following example show the contents of data.csv

key,value
foo,bar
ding,dong

The following feature shows how to use CSV data.

Feature: External CSV Data
    Scenario Outline: CSV Scenario Outline
        Given there is a csv-data-file with <key> and <value>

    Examples: CSV Data-Source
        CSV: data.csv

Running the examples leads to multiple dynamically generated scenarios with parameters:

Scenario: CSV Scenario Outline [key=foo, value=bar] (Feature: ...)
Scenario: CSV Scenario Outline [key=ding, value=dong] (Feature: ...)

JSON Data Sources

JSON data sources are also supported via the JSON: keyword followed by a JSON filename.

The filename can be an absolute path or a relative path. Relative paths are relative to the feature file where they appear.

The following example show the contents of data.json

[
    {"key": "foo", "value": "bar"},
    {"key": "ding", "value": "dong"}
]

The contents have to be a list of dictionaries.

The following feature shows how to use CSV data.

Feature: External JSON Data
    Scenario Outline: JSON Scenario Outline
        Given there is a json-data-file with <key> and <value>

    Examples: JSON Data-Source
        JSON: data.json

Running the examples leads to multiple dynamically generated scenarios with parameters:

Scenario: JSON Scenario Outline [key=foo, value=bar] (Feature: ...)
Scenario: JSON Scenario Outline [key=ding, value=dong] (Feature: ...)

YAML Data Sources

YAML data sources are also supported via the YAML: keyword followed by a YAML filename.

The filename can be an absolute path or a relative path. Relative paths are relative to the feature file where they appear.

The following example show the contents of data.yml

- key: foo
  value: bar
- key: ding
  value: dong

The contents have to be a list of dictionaries.

The following feature shows how to use CSV data.

Feature: External YAML Data
    Scenario Outline: YAML Scenario Outline
        Given there is a yaml-data-file with <key> and <value>

    Examples: YAML Data-Source
        YAML: data.yml

Running the examples leads to multiple dynamically generated scenarios with parameters:

Scenario: YAML Scenario Outline [key=foo, value=bar] (Feature: ...)
Scenario: YAML Scenario Outline [key=ding, value=dong] (Feature: ...)

Python Generators Data Sources

Python generators can also be used as data sources via the Generator: keyword followed by a generator title.

Every data generator needs to be decorated with htf.data_generator. The following Python code can be used as data sources.

@htf.data_generator("Python-based Generator")
def data_generator() -> Generator[Dict[str, str], None, None]:
    yield dict(key="foo", value="bar")  # this is the most efficient way
    yield dict(key="ding", value="dong")
    # ...

@htf.data_generator("Python-based Data Source with Lists")
def data_source_with_list() -> List[Dict[str, str]]:
    return [  # this also works
        dict(key="foo", value="bar"),
        dict(key="ding", value="dong")
        # ...
    ]

Async Python generators are supported, too.

@htf.data_generator("Python-based Generator")
async def data_generator() -> AsyncGenerator[Dict[str, str], None]:
    yield dict(key="foo", value="bar")  # this is the most efficient way
    yield dict(key="ding", value="dong")
    # ...

The data generator must return an iterable so you can make it a Python generator or let it return an iterable object like a list for example.

The following feature shows how to use a Python generator as a data source.

Feature: Python Generator Data Source
    Scenario Outline: Data Source Scenario Outline
        Given there is a data-source yielding <key> and <value>

    Examples: Python Generator Data-Source
        Generator: Python-based Generator

Running the examples leads to multiple dynamically generated scenarios with parameters:

Scenario: Data Source Scenario Outline [key=foo, value=bar] (Feature: ...)
Scenario: Data Source Scenario Outline [key=ding, value=dong] (Feature: ...)
htf.data_generator(pattern: str, parser: str | None = None) Callable[[Callable[[...], Any]], Any]

Decorates a method to make it an implementation of a data generator used for examples in a behavior driven test.

Parameters:
  • pattern – the pattern to match the title

  • parser=None – the parser to match the name of the example

Multiple and Combined Data Sources

Every scenario outline can have multiple data sources that can also be mixed.

Feature: Mixed External Data
    Scenario Outline: Scenario Outline with Mixed Data
        Given there is a data-table with <key> and <value>

    Examples:
        |  key | value |
        |  foo |   bar |
        # CSV: data.csv
        # JSON: data.json
        # YAML: data.yml
        # Generator: Python-based Generator
        # etc.

Transforming Data Sources

Sometimes you need to precalculate values based on data-sources. To do this HILSTER Testing Framework supports data-transformation.

A data transformation is created by decorating a method with the htf.transformation decorator. The method is called for every line of the data source. To use a data-transformation the data source (Example:) need to have a name supplied.

In the following example the Addition functionality for a calculator is tested. The data-source only contains to addends but not the required result. To check if the calculator is working properly the result needs to be precalculated.

Feature: Addition

  Scenario Outline: Addition
    Given the calculator is cleared
    When <a> is added to <b>
    And the equals button is pressed
    Then the result is <result>

    Examples: Addition
      |   a |   b |
      |   1 |   1 |
      |   2 |  12 |

To implement a data-transformation hook you could use the following code:

@htf.transformation("Addition")
def expected_addition_result(data):
    data["result"] = int(data["a"]) + int(data["b"])
    return data

Running the examples leads to multiple dynamically generated scenarios with parameters where result is generated on the fly.

Transformations can be async, too.

@htf.transformation("Addition")
async def expected_addition_result(data):
    data["result"] = int(data["a"]) + int(data["b"])
    return data
Scenario: Addition [a=1, b=2, result=3] (Feature: ...)
Scenario: Addition [a=2, b=12, result=14] (Feature: ...)
htf.transformation(pattern: str, parser: str | None = None) Callable[[Callable[[...], Any]], Any]

Decorates a method to make it an implementation of a data transformation hook used for examples in a behavior driven test.

Parameters:
  • pattern – gherkin examples pattern

  • parser=None – the parser to match the name of the example

Skipping Features, Scenarios or Scenario Outlines

You can skip features, scenarios or scenario outlines by adding a line Skip: <reason> to your feature.

To skip a whole feature you can use:

Feature: Skipped Feature
    Skip: this feature is skipped
    Scenario: Skipped Feature Scenario
        Given there is a skipped feature

To skip a scenario you can use:

Feature: Skipped Feature
    Scenario: Skipped Feature Scenario
        Skip: this scenario is skipped
        Given there is a skipped feature

To skip a scenario outline you can use:

Feature: Skipped Feature
    Scenario Outline: Skipped Feature Scenario Outline
        Skip: this scenario is skipped
        Given there is a skipped feature

        Examples:
        |  key | value |
        |  foo |   bar |

Setup and Tear Down Code

It is possible to create setup and tear down code for features and scenarios, too.

To do this you can use htf.feature and htf.scenario.

The decorated methods can be returning methods to only implement setup code or can be implemented as generators yielding one element to implement setup and tear down code.

Fixtures and parameters are added like for the other step implementations.

Feature: Feature with Setup and Tear Down code

    Scenario: Setup and Tear Down
        Given the setup was run
        when actions are performed
        then an outcome will be available

In the following example only setup code for a feature and a scenario is implemented.

@htf.feature("Feature with Setup and Tear Down code")
def feature_set_up(context):
    # your feature setup code comes here

@htf.scenario("Setup and Tear Down")
def scenario_set_up(context):
    # your scenario setup code comes here

And in the following example setup and tear down code is implemented.

@htf.feature("Feature with Setup and Tear Down code")
def feature_set_up_and_tear_down(context):
    # your feature setup code comes here
    yield  # now the scenario is run
    # your feature tear down code comes here

@htf.scenario("Setup and Tear Down")
def scenario_set_up(context):
    # your scenario setup code comes here
    yield  # now the scenario is run
    # your scenario tear down code comes here

Setup and tear down code can be async, too.

htf.feature(pattern: str, parser: str | None = None, extra_types: Dict[Any, Any] | None = None, converters: Dict[str, Any] | None = None) Callable[[Callable[[...], Any]], Any]

Decorates a method to make it an implementation of a given feature in a behavior driven test.

It can be implemented as a normal method with a return value to implement set up code or as a generator to realize set up and tear down code for a feature.

Parameters:
  • pattern – gherkin feature pattern

  • parser=None – the parser to parse the parameters from the precondition

  • extra_types=None – extra types for parsing

  • converters=None – optional converters that are applied to parameter after parsing

htf.scenario(pattern: str, parser: str | None = None, extra_types: Dict[Any, Any] | None = None, converters: Dict[str, Any] | None = None) Callable[[Callable[[...], Any]], Any]

Decorates a method to make it an implementation of a given scenario in a behavior driven test.

It can be implemented as a normal method with a return value to implement set up code or as a generator to realize set up and tear down code for a scenario.

Parameters:
  • pattern – gherkin feature pattern

  • parser=None – the parser to parse the parameters from the precondition

  • extra_types=None – extra types for parsing

  • converters=None – optional converters that are applied to parameters after parsing

Parsing Steps

HILSTER Testing Framework offers three different parsers to parse steps and to find the implementation for a step by its name.

The default parser is based on the parse module. There is an extended version of parse called cfparse. And there is also a regular-expression parser (re).

All parsers support pattern matching and type casting.

How Parsing and Matching Works

HILSTER Testing Framework collects all implemented steps and features. When features, scenarios and steps are run the implementing step is looked up by iterating over all collected steps with the corresponding scope and matching the current step to the pattern using a parser.

If there is one match the step will be executed with optional parameters extracted. Parameters can also be converted to other types before calling the implementing method.

Using the Default Parser (parse)

The default step parser is based on the parse packet that is the opposite of Python’s format method. It supports the Format String Syntax.

Parameters are put between { and } (e.g. {number}) and can have type casts in the format expression in the form of {[parameter name]:[format specification]}.

The parsed parameters are passed to the implementing method by its name.

The following Gherkin example describes addition.

Feature: Addition

  Scenario: Addition
    Given the calculator is cleared
    When 1 is added to 2
    And the equals button is pressed
    Then the result is <result>

The following example parses a and b that are added as strings that are added.

@htf.when("{a} is added to {b}")
def add(a, b, context):
    context.expected_result = int(a) + int(b)  # this type-cast has to be performed here!

In the example the parameters a and b are passed as strings so they need to be converted to int in the implementation.

To automatically convert a and b to int while parsing you can change the format string to:

@htf.when("{a:g} is added to {b:g}")
def add(a, b, context):
    context.expected_result = a + b  # a and b are int

You can find more information about the type conversion on GitHub parse repository.

Using the Regular-Expression-Parser (re, regex)

The re parser uses regular-expressions to do the parsing. Thus it is the most flexible parser but is more complex.

To enable the CF-Parser the parser argument of the step-decorator has to be set with cf or cfparse and converters can to be supplied optionally.

converters is a dictionary of callables where the keys are the names of the parameters of the implementing method.

The regular-expression must make use of named subgroups (e.g. r"(?P<parameter name>\w+)"). You can find more information about named subgroups in the Python re Module.

Feature: Addition

  Scenario: Addition
    Given the calculator is cleared
    When 1 is added to 2
    And the equals button is pressed
    Then the result is <result>

The following example parses a and b that are added as strings that are added.

@htf.when(r"(?P<a>\d+) is added to (?P<b>\d+)", parser="re")
def add(a, b, context):
    context.expected_result = int(a) + int(b)  # this type-cast has to be performed here!

In the example the parameters a and b are passed as strings so they need to be converted to int in the implementation.

To automatically convert a and b to int while parsing you can change the format string to:

@htf.when(r"(?P<a>\d+) is added to (?P<b>\d+)", parser="re", converters=dict(a=int, b=int)))
def add(a, b, context):
    context.expected_result = a + b  # a and b are int

Using the CF-Parser (cfparse, cf)

The CF-Parser extends the default with support for cardinality fields, and it automatically creates type variants to parse repeated parameters.

To enable the CF-Parser the parser argument of the step-decorator has to be set with cf or cfparse and extra_types have to be supplied, too.

The following examples show how to accumulate a list of integers with a variable length.

The following example accumulates a list of numbers with variable amount.

Feature: Accumulation
  Scenario: Accumulate 1
    When 1 is accumulated
    Then the result is 1

  Scenario: Accumulate 2
    When 1, 2 is accumulated
    Then the result is 3

  Scenario: Accumulate many
    When 1, 2, 3, 4, 5, 6, 7, 8, 9 is accumulated
    Then the result is 45

The Python-implementation looks like

import parse

@parse.with_pattern(r"\d+")
def parse_number(text):
     return int(text)

extra_types = dict(Number=parse_number)

@htf.when("{numbers:Number+} is accumulated", parser="cf", extra_types=extra_types)
def accumulate(numbers, context):
    context.accumulated_numbers = sum(numbers)

@htf.then("the result is {result:g}")
def the_result_is(result, context):
    htf.assert_equal(result, context.accumulated_numbers)

You can find more information about cardinality field support on GitHub parse-type repository.

Getting Available Steps and Statements

As a developer you need to get all available steps and statements without looking into the code all the time.

This can be done using htf.get_statements and htf statements <steps/test specifiers>.

On the command-line you can run:

htf statements steps.py

Collecting statements

Given statements:
Given the refrigeration machine controller is running at {speed:g} rpm

When conditions:
When the initial temperature is {temparature:g} °C
When {numbers:Number+} is accumulated

Then assertions:
Then the result is {result:g}
Then the refrigeration machine controller is cooling down
Then the refrigeration machine controller does not cool down

Features:
.*

Scenarios:
.*

Transformations:
X and Y data

Data Generators:
Data Generator

And from Python you can use htf.get_statements:

htf.get_statements("steps.py")
>>> {
    'given': [
        'Given the refrigeration machine controller is running at {speed:g} rpm'
    ],
    'when': [
        'When the initial temperature is {temparature:g} °C',
        'When {numbers:Number+} is accumulated'
    ],
    'then': [
        'Then the refrigeration machine controller is cooling down',
        'Then the refrigeration machine controller does not cool down',
        'Then the result is {result:g}'
    ],
    'features': ['.*'],
    'scenarios': ['.*'],
    'transformations': ['X and Y data'],
    'data_generators': ['Data Generator']
}
htf.get_statements(tests: Any | List[Any] = '__main__') Dict[str, List[str]]

Get a dict of behavior driven development statements found in the specified tests.

Parameters:

tests=None – a test-specifier or a list of test-specifiers (folder, file, test-case, test-case-method, module)

Returns:

a dict with all behavior driven development statements found

Return type:

dict

Examples

You can find examples in the htf-demo, too.