Cardinality: Zero or More (List of Type)

The solution to this problem is basically the same like with Cardinality: One or More (List of Type). Note that the case for zero or more items is not so often needed.

Initially, a comma-separated list is processed, like:

Scenario:
    When I paint with red, green

Next, a list that is separated with the word “and” is processed, like:

Scenario:
    When I paint with red and green

Feature Example

# file:datatype.features/cardinality.zero_or_more.feature
Feature: Data Type with Cardinality zero or more (MANY0, List<T>)

  Scenario: Empty list, comma-separated
    Given I am a painter
    When I paint with
    Then no colors are used

  Scenario: List with one item, comma-separated
    Given I am a painter
    When I paint with blue
    Then the following colors are used:
      | color |
      | blue  |

  Scenario: Many list, comma-separated
    Given I am a painter
    When I paint with red, green
    Then the following colors are used:
      | color |
      | red   |
      | green |

  Scenario: Many list with list-separator "and"
    Given I am a painter
    When I paint with red and green and blue
    Then the following colors are used:
      | color |
      | red   |
      | green |
      | blue  |

Define the Data Type

# file:datatype.features/steps/step_cardinality_zero_or_more.py
# ------------------------------------------------------------------------
# USER-DEFINED TYPES:
# ------------------------------------------------------------------------
from behave import register_type
from parse_type import TypeBuilder

def slurp_space(text):
    return text
slurp_space.pattern = r"\s*"
register_type(slurp_space=slurp_space)

parse_color = TypeBuilder.make_choice([ "red", "green", "blue", "yellow" ])
register_type(Color=parse_color)

# -- MANY-TYPE: Persons := list<Person> with list-separator = "and"
# parse_colors = TypeBuilder.with_many0(parse_color, listsep="and")
parse_colors0A= TypeBuilder.with_zero_or_more(parse_color, listsep="and")
register_type(OptionalColorAndMore=parse_colors0A)

# -- NEEDED-UNTIL: parse_type.cfparse.Parser is used by behave.
# parse_colors0C = TypeBuilder.with_zero_or_more(parse_color)
# type_dict = {"Color*": parse_colors0C}
# register_type(**type_dict)


Note

The TypeBuilder.with_zero_and_more() function performs the magic. It computes a regular expression pattern for the list of items. Then it generates a type-converter function that processes the list of items by using the type-converter for one item (“Color”).

Provide the Step Definitions

# file:datatype.features/steps/step_cardinality_zero_or_more.py
# ----------------------------------------------------------------------------
# STEPS:
# ----------------------------------------------------------------------------
from behave import given, when, then

# -- MANY-VARIANT 1: Use Cardinality field in parse expression (comma-separated)
@when(u'I paint with{:slurp_space}{colors:Color*}')
def step_when_I_paint_with_colors(context, _, colors):
    for color in colors:
        context.used_colors.add(color)

# -- MANY-VARIANT 2: Use special many data type ("and"-separated)
@when(u'I paint with{:slurp_space}{colors:OptionalColorAndMore}')
def step_when_I_paint_with_color_and_more(context, _, colors):
    for color in colors:
        context.used_colors.add(color)


# ----------------------------------------------------------------------------
# MORE STEPS:
# ----------------------------------------------------------------------------
from hamcrest import assert_that, contains, has_length

@given('I am a painter')
def step_given_I_am_a_painter(context):
    context.used_colors = set()

@then('no colors are used')
def step_then_no_colors_are_used(context):
    assert_that(context.used_colors, has_length(0))

@then('the following colors are used')
def step_then_following_colors_are_used(context):
    assert context.table, "table<color> is required"
    used_colors     = sorted(context.used_colors)
    expected_colors = [ row[0]  for row in context.table ]
    # -- LIST-COMPARISON:
    assert_that(used_colors, contains(*sorted(expected_colors)))

Run the Test

Now we run this example with behave:

$ behave ../datatype.features/cardinality.zero_or_more.feature
Feature: Data Type with Cardinality zero or more (MANY0, List<T>)   # ../datatype.features/cardinality.zero_or_more.feature:1

  Scenario: Empty list, comma-separated  # ../datatype.features/cardinality.zero_or_more.feature:3
    Given I am a painter                 # ../datatype.features/steps/step_cardinality_zero_or_more.py:73
    When I paint with                    # ../datatype.features/steps/step_cardinality_zero_or_more.py:56
    Then no colors are used              # ../datatype.features/steps/step_cardinality_zero_or_more.py:77

  Scenario: List with one item, comma-separated  # ../datatype.features/cardinality.zero_or_more.feature:8
    Given I am a painter                         # ../datatype.features/steps/step_cardinality_zero_or_more.py:73
    When I paint with blue                       # ../datatype.features/steps/step_cardinality_zero_or_more.py:56
    Then the following colors are used           # ../datatype.features/steps/step_cardinality_zero_or_more.py:81
      | color |
      | blue  |

  Scenario: Many list, comma-separated  # ../datatype.features/cardinality.zero_or_more.feature:15
    Given I am a painter                # ../datatype.features/steps/step_cardinality_zero_or_more.py:73
    When I paint with red, green        # ../datatype.features/steps/step_cardinality_zero_or_more.py:56
    Then the following colors are used  # ../datatype.features/steps/step_cardinality_zero_or_more.py:81
      | color |
      | red   |
      | green |

  Scenario: Many list with list-separator "and"  # ../datatype.features/cardinality.zero_or_more.feature:23
    Given I am a painter                         # ../datatype.features/steps/step_cardinality_zero_or_more.py:73
    When I paint with red and green and blue     # ../datatype.features/steps/step_cardinality_zero_or_more.py:62
    Then the following colors are used           # ../datatype.features/steps/step_cardinality_zero_or_more.py:81
      | color |
      | red   |
      | green |
      | blue  |

1 feature passed, 0 failed, 0 skipped
4 scenarios passed, 0 failed, 0 skipped
12 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.003s

The Complete Picture

# file:datatype.features/steps/step_cardinality_zero_or_more.py
# -*- coding: UTF-8 -*-
"""
Feature: Data Type with Cardinality zero or more (MANY0, List<T>)

  Scenario: Many list, comma-separated
    Given I am a painter
    When I paint with red, green
    Then the following colors are used:
      | color |
      | red   |
      | green |

  Scenario: Many list with list-separator "and"
    Given I am a painter
    When I paint with red and green and blue
    Then the following colors are used:
      | color |
      | red   |
      | green |
      | blue  |
"""

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

def slurp_space(text):
    return text
slurp_space.pattern = r"\s*"
register_type(slurp_space=slurp_space)

parse_color = TypeBuilder.make_choice([ "red", "green", "blue", "yellow" ])
register_type(Color=parse_color)

# -- MANY-TYPE: Persons := list<Person> with list-separator = "and"
# parse_colors = TypeBuilder.with_many0(parse_color, listsep="and")
parse_colors0A= TypeBuilder.with_zero_or_more(parse_color, listsep="and")
register_type(OptionalColorAndMore=parse_colors0A)

# -- NEEDED-UNTIL: parse_type.cfparse.Parser is used by behave.
# parse_colors0C = TypeBuilder.with_zero_or_more(parse_color)
# type_dict = {"Color*": parse_colors0C}
# register_type(**type_dict)


# @mark.steps
# ----------------------------------------------------------------------------
# STEPS:
# ----------------------------------------------------------------------------
from behave import given, when, then

# -- MANY-VARIANT 1: Use Cardinality field in parse expression (comma-separated)
@when(u'I paint with{:slurp_space}{colors:Color*}')
def step_when_I_paint_with_colors(context, _, colors):
    for color in colors:
        context.used_colors.add(color)

# -- MANY-VARIANT 2: Use special many data type ("and"-separated)
@when(u'I paint with{:slurp_space}{colors:OptionalColorAndMore}')
def step_when_I_paint_with_color_and_more(context, _, colors):
    for color in colors:
        context.used_colors.add(color)


# ----------------------------------------------------------------------------
# MORE STEPS:
# ----------------------------------------------------------------------------
from hamcrest import assert_that, contains, has_length

@given('I am a painter')
def step_given_I_am_a_painter(context):
    context.used_colors = set()

@then('no colors are used')
def step_then_no_colors_are_used(context):
    assert_that(context.used_colors, has_length(0))

@then('the following colors are used')
def step_then_following_colors_are_used(context):
    assert context.table, "table<color> is required"
    used_colors     = sorted(context.used_colors)
    expected_colors = [ row[0]  for row in context.table ]
    # -- LIST-COMPARISON:
    assert_that(used_colors, contains(*sorted(expected_colors)))