Cardinality: One or More (List of Type)

Sometimes a solution is needed where a list of one or more items needs to be parsed. Initially, this should be a comma-separated list, like:

Scenario:
    When I meet Alice
     And I meet Alice, Bob, Charly

Then, a list should be processed that is separated by the word “and”, like:

Scenario:
    When I meet Alice and Bob and Charly

Feature Example

# file:datatype.features/cardinality.one_or_more.feature
Feature: Data Type with Cardinality one or more (MANY, List<T>)

  Scenario: Many list, comma-separated
    Given I go to a meeting
    When I meet Alice, Bob, Dodo
    And  I meet Charly
    Then the following persons are present:
      | name   |
      | Alice  |
      | Bob    |
      | Charly |
      | Dodo   |

  Scenario: Many list with list-separator "and"
    Given I go to a meeting
    When I meet Alice and Bob and Charly
    Then the following persons are present:
      | name   |
      | Alice  |
      | Bob    |
      | Charly |

Define the Data Type

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

company_persons = [ "Alice", "Bob", "Charly", "Dodo" ]
parse_person = TypeBuilder.make_choice(company_persons)
register_type(Person=parse_person)

# -- MANY-TYPE: Persons := list<Person> with list-separator = "and"
# parse_persons = TypeBuilder.with_one_or_more(parse_person, listsep="and")
parse_persons = TypeBuilder.with_many(parse_person, listsep="and")
register_type(PersonAndMore=parse_persons)

# -- NEEDED-UNTIL: parse_type.cfparse.Parser is used by behave.
# parse_persons2 = TypeBuilder.with_many(parse_person)
# type_dict = {"Person+": parse_persons2}
# register_type(**type_dict)


Note

The TypeBuilder.with_many() 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 (“Person”).

Provide the Step Definitions

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

# -- MANY-VARIANT 1: Use Cardinality field in parse expression (comma-separated)
@when('I meet {persons:Person+}')
def step_when_I_meet_persons(context, persons):
    for person in persons:
        context.meeting.persons.add(person)

# -- MANY-VARIANT 2: Use special many data type ("and"-separated)
@when('I meet {persons:PersonAndMore}')
def step_when_I_meet_person_and_more(context, persons):
    for person in persons:
        context.meeting.persons.add(person)

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

@given('I go to a meeting')
def step_given_I_go_to_meeting(context):
    context.meeting = Meeting()

@then('the following persons are present')
def step_following_persons_are_present(context):
    assert context.table, "table<Person> is required"
    actual_persons   = sorted(context.meeting.persons)
    expected_persons = [ row["name"]  for row in context.table ]

    # -- LIST-COMPARISON:
    assert_that(actual_persons, contains(*expected_persons))

Run the Test

Now we run this example with behave:

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

  Scenario: Many list, comma-separated     # ../datatype.features/cardinality.one_or_more.feature:3
    Given I go to a meeting                # ../datatype.features/steps/step_cardinality_one_or_more.py:79
    When I meet Alice, Bob, Dodo           # ../datatype.features/steps/step_cardinality_one_or_more.py:63
    And I meet Charly                      # ../datatype.features/steps/step_cardinality_one_or_more.py:63
    Then the following persons are present # ../datatype.features/steps/step_cardinality_one_or_more.py:83
      | name   |
      | Alice  |
      | Bob    |
      | Charly |
      | Dodo   |

  Scenario: Many list with list-separator "and"  # ../datatype.features/cardinality.one_or_more.feature:14
    Given I go to a meeting                      # ../datatype.features/steps/step_cardinality_one_or_more.py:79
    When I meet Alice and Bob and Charly         # ../datatype.features/steps/step_cardinality_one_or_more.py:69
    Then the following persons are present       # ../datatype.features/steps/step_cardinality_one_or_more.py:83
      | name   |
      | Alice  |
      | Bob    |
      | Charly |

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

The Complete Picture

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

  Scenario:
    Given I go to a meeting
    When I meet Alice, Bob, Charly
    And  I meet Dodo
    Then the following persons are present:
      | name   |
      | Alice  |
      | Bob    |
      | Charly |
      | Dodo   |

  Scenario: Many list with list-separator "and"
    Given I go to a meeting
    When I meet Alice and Bob and Charly
    Then the following persons are present:
      | name   |
      | Bob    |
      | Alice  |
      | Charly |
"""

# ----------------------------------------------------------------------------
# DOMAIN MODEL:
# ----------------------------------------------------------------------------
class Meeting(object):
    def __init__(self):
        self.persons = set()


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

company_persons = [ "Alice", "Bob", "Charly", "Dodo" ]
parse_person = TypeBuilder.make_choice(company_persons)
register_type(Person=parse_person)

# -- MANY-TYPE: Persons := list<Person> with list-separator = "and"
# parse_persons = TypeBuilder.with_one_or_more(parse_person, listsep="and")
parse_persons = TypeBuilder.with_many(parse_person, listsep="and")
register_type(PersonAndMore=parse_persons)

# -- NEEDED-UNTIL: parse_type.cfparse.Parser is used by behave.
# parse_persons2 = TypeBuilder.with_many(parse_person)
# type_dict = {"Person+": parse_persons2}
# 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('I meet {persons:Person+}')
def step_when_I_meet_persons(context, persons):
    for person in persons:
        context.meeting.persons.add(person)

# -- MANY-VARIANT 2: Use special many data type ("and"-separated)
@when('I meet {persons:PersonAndMore}')
def step_when_I_meet_person_and_more(context, persons):
    for person in persons:
        context.meeting.persons.add(person)

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

@given('I go to a meeting')
def step_given_I_go_to_meeting(context):
    context.meeting = Meeting()

@then('the following persons are present')
def step_following_persons_are_present(context):
    assert context.table, "table<Person> is required"
    actual_persons   = sorted(context.meeting.persons)
    expected_persons = [ row["name"]  for row in context.table ]

    # -- LIST-COMPARISON:
    assert_that(actual_persons, contains(*expected_persons))