Use Optional Part in Step Definitions

It is a common case that optional parts occur in steps and step definitions. This happens often when you start writing steps and step definitions.

EXAMPLE:

Feature:
    Scenario: Case 1
        When attacked by a samurai
        ...

    Scenario: Case 2
        When attacked by Chuck Norris
        ...

It would be nice if only one step definition would be sufficient for both cases. An other point is that the step definition implementation is also identical.

Variant 1: Use Cardinality Field

The parse expression format provides an optional cardinality field part after the type field. The ‘?’ character is used to mark a step parameter as optional (cardinality: 0..1).

Note

You need to use the “cfparse” step matcher (parse with cardinality field support based on a parse_type parser) to use this functionality. The cardinality field is optional and follows the type field “Person” in a parse expression, like:

Cardinality Example Description
1 {person:Person} One, implicit without cardinality field.
0..1 {person:Person?} Zero or one: For optional parts.
0..* {persons:Person*} Zero or more: For list<T> (many0).
1..* {persons:Person+} One or more: For list<T> (many).

Using the cardinality field in a parse expression is the preferred solution for the optional parameter problem.

Provide the Step Definitions

# file:step_matcher.features/steps/step_optional_part.py
# ----------------------------------------------------------------------------
# STEPS:
# ----------------------------------------------------------------------------
from behave import given, when, then
from hamcrest import assert_that, equal_to, is_in

# -- OPTIONAL-PART: {:a_?}
# By using cardinality field in parse expressions.
@when('attacked by {:a_?}{opponent}')
def step_attacked_by(context, a_, opponent):
    context.ninja_fight.opponent = opponent
    # -- VERIFY: Optional part feature.
    assert_that(a_, is_in(["a", None]))
    assert_that(opponent, is_in(["Chuck Norris", "samurai"]))

Define the Data Type

# file:step_matcher.features/steps/step_optional_part.py
# ------------------------------------------------------------------------
# USER-DEFINED TYPES:
# ------------------------------------------------------------------------
from behave import register_type
import parse

@parse.with_pattern(r"a\s+")
def parse_word_a(text):
    """Type converter for "a " (followed by one/more spaces)."""
    return text.strip()

register_type(a_=parse_word_a)

# -- NEEDED-UNTIL: parse_type.cfparse.Parser is used by behave.
# from parse_type import TypeBuilder
# parse_opt_word_a = TypeBuilder.with_optional(parse_word_a)
# type_dict = {"a_?": parse_opt_word_a}
# register_type(**type_dict)


Run the Test

Now we run this example with behave:

$ behave ../step_matcher.features/use_optional_part.feature
Feature: Use Optional Part in Step Definitions   # ../step_matcher.features/use_optional_part.feature:1

  Scenario: Case1 "When attacked by a ..."  # ../step_matcher.features/use_optional_part.feature:3
    Given the ninja has a black-belt        # ../step_matcher.features/steps/step_optional_part.py:57
    When attacked by a samurai              # ../step_matcher.features/steps/step_optional_part.py:44

  Scenario: Case2 "When attacked by ..."  # ../step_matcher.features/use_optional_part.feature:7
    Given the ninja has a black-belt      # ../step_matcher.features/steps/step_optional_part.py:57
    When attacked by Chuck Norris         # ../step_matcher.features/steps/step_optional_part.py:44

1 feature passed, 0 failed, 0 skipped
2 scenarios passed, 0 failed, 0 skipped
4 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.001s

The Complete Picture

# file:step_matcher.features/steps/step_optional_part.py
# -*- coding: UTF-8 -*-
"""
Feature: Use Optional Part in Step Definitions

  Scenario: Case 1 with "a "
    Given ...
    When attacked by a samurai

  Scenario: Case 2 without "a "
    Given ...
    When attacked by Chuck Norris
"""

# @mark.user_defined_types
# ------------------------------------------------------------------------
# USER-DEFINED TYPES:
# ------------------------------------------------------------------------
from behave import register_type
import parse

@parse.with_pattern(r"a\s+")
def parse_word_a(text):
    """Type converter for "a " (followed by one/more spaces)."""
    return text.strip()

register_type(a_=parse_word_a)

# -- NEEDED-UNTIL: parse_type.cfparse.Parser is used by behave.
# from parse_type import TypeBuilder
# parse_opt_word_a = TypeBuilder.with_optional(parse_word_a)
# type_dict = {"a_?": parse_opt_word_a}
# register_type(**type_dict)


# @mark.steps
# ----------------------------------------------------------------------------
# STEPS:
# ----------------------------------------------------------------------------
from behave import given, when, then
from hamcrest import assert_that, equal_to, is_in

# -- OPTIONAL-PART: {:a_?}
# By using cardinality field in parse expressions.
@when('attacked by {:a_?}{opponent}')
def step_attacked_by(context, a_, opponent):
    context.ninja_fight.opponent = opponent
    # -- VERIFY: Optional part feature.
    assert_that(a_, is_in(["a", None]))
    assert_that(opponent, is_in(["Chuck Norris", "samurai"]))

# @mark.more_steps
# ----------------------------------------------------------------------------
# MORE STEPS:
# ----------------------------------------------------------------------------
from ninja_fight import NinjaFight

@given('the ninja has a {achievement_level}')
def step_the_ninja_has_a(context, achievement_level):
    context.ninja_fight = NinjaFight(achievement_level)

@then('the ninja should {reaction}')
def step_the_ninja_should(context, reaction):
    assert_that(reaction, equal_to(context.ninja_fight.decision()))

Variant 2: Use Data Type with Cardinality 0..1

A special data type must be defined and registered that has the cardinality zero or one (0..1). This is similar to the solution above. But it requires that the developer performs this work. In the case above this task is implicitly done by parse when it was needed (triggered by the cardinality field).

Hint

See also Cardinality: Zero or One (Optional) for more information.