Use Multi-Methods in Step Definitions

Assume you have a number of rather similar steps, like:

# file:step_matcher.features/use_multi_methods.feature
Feature: Use Multi-Methods in Step Definitions
  Scenario:
    Given I go to a shop
    When I buy 2 cucumbers
     And I buy 3 apples
     And I buy 4 diamonds

But you need different step definition implementations for some cases (data types, actually their regular expressions). In this example, the following cases should be distinguished:

  • vegetables
  • fruits
  • anything else

There are 2 possible solutions how this problem can be mapped into step definitions.

Variant 1: Use Single Method

One step definition with a string-based data type is provided in this solution. The step definition implementation contains the logic how to distinguish between the different cases.

# -- FILE: step_matcher.features/steps/one_step.py
from behave import given, when, then

@when("I buy {amount:n} {shop_item:w}")
def step_when_I_buy_shop_item(context, amount, shop_item):
    pass    # -- HERE comes the logic how to distinguish the cases.

Variant 2: Use Multi-Methods

If different data types are needed in the step definitions, another solution may be better. This solution, the multi-methods approach, is described here.

Caution

This solution requires that each case uses a different regular expression for each data type (including the else-case). Otherwise, the step matcher algorithm will not be able to distinguish these cases.

Provide the Step Definitions

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

@given(u"I go to a shop")
def step_given_I_go_to_a_shop(context):
    context.shop = Shop()
    context.shopping_cart = [ ]

# -- STEP-ORDERING-IMPORTANT: Else step must be last.
@when(u"I buy {amount:n} {vegetable:Vegetable}")
def step_when_I_buy_vegetable(context, amount, vegetable):
    price = context.shop.calculate_price_for_vegetable(vegetable, amount)
    context.shopping_cart.append((vegetable, amount, price))

@when(u"I buy {amount:n} {fruit:Fruit}")
def step_when_I_buy_fruit(context, amount, fruit):
    price = context.shop.calculate_price_for_fruit(fruit, amount)
    context.shopping_cart.append((fruit, amount, price))

@when(u"I buy {amount:n} {anything_else:w}")
def step_when_I_buy_anything_else(context, amount, anything_else):
    price = context.shop.calculate_price_for(anything_else, amount)
    context.shopping_cart.append((anything_else, amount, price))

Define the Data Types

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

parse_vegetable = TypeBuilder.make_choice(["cucumbers", "lettuce"])
register_type(Vegetable=parse_vegetable)

parse_fruit = TypeBuilder.make_choice(["apples", "pears"])
register_type(Fruit=parse_fruit)

Run the Test

Now we run this example with behave:

$ behave ../step_matcher.features/use_multi_methods.feature
Feature: Use Multi-Methods in Step Definitions   # ../step_matcher.features/use_multi_methods.feature:1

  Scenario:                # ../step_matcher.features/use_multi_methods.feature:2
    Given I go to a shop   # ../step_matcher.features/steps/step_multi_methods.py:60
    When I buy 2 cucumbers # ../step_matcher.features/steps/step_multi_methods.py:66
    And I buy 3 apples     # ../step_matcher.features/steps/step_multi_methods.py:71
    And I buy 4 diamonds   # ../step_matcher.features/steps/step_multi_methods.py:76

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

Note

Notice the difference in line numbers for each step. Each step matches a different step definition (implementation).

The Complete Picture

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# file:step_matcher.features/steps/step_multi_methods.py
# -*- coding: UTF-8 -*-
"""
Feature: Use Multi-Methods in Step Definitions

    Scenario:
        Given I go to a shop
        When I buy 2 cucumbers
         And I buy 3 apples
         And I buy 4 diamonds
"""

# @mark.domain_model
# ------------------------------------------------------------------------
# DOMAIN MODEL:
# ------------------------------------------------------------------------
class Shop(object):
    vegetable_price_list = {
        "cucumbers": 0.2,   # Dollars per piece.
        "lettuce":   0.8,   # Dollars per piece.
    }
    fruit_price_list = {
        "apples":     0.5,  # Dollars per piece.
        "pears":      0.6,  # Dollars per piece.
    }
    common_price_list = {
        "diamonds": 1000.    # Dollars for one with 10 karat (only 1 size).
    }

    def calculate_price_for_fruit(self, fruit, amount):
        price_per_unit = self.fruit_price_list[fruit]
        return price_per_unit*amount

    def calculate_price_for_vegetable(self, vegetable, amount):
        price_per_unit = self.vegetable_price_list[vegetable]
        return price_per_unit*amount

    def calculate_price_for(self, shop_item, amount):
        price_per_unit = self.common_price_list[shop_item]
        return price_per_unit*amount

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

parse_vegetable = TypeBuilder.make_choice(["cucumbers", "lettuce"])
register_type(Vegetable=parse_vegetable)

parse_fruit = TypeBuilder.make_choice(["apples", "pears"])
register_type(Fruit=parse_fruit)

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

@given(u"I go to a shop")
def step_given_I_go_to_a_shop(context):
    context.shop = Shop()
    context.shopping_cart = [ ]

# -- STEP-ORDERING-IMPORTANT: Else step must be last.
@when(u"I buy {amount:n} {vegetable:Vegetable}")
def step_when_I_buy_vegetable(context, amount, vegetable):
    price = context.shop.calculate_price_for_vegetable(vegetable, amount)
    context.shopping_cart.append((vegetable, amount, price))

@when(u"I buy {amount:n} {fruit:Fruit}")
def step_when_I_buy_fruit(context, amount, fruit):
    price = context.shop.calculate_price_for_fruit(fruit, amount)
    context.shopping_cart.append((fruit, amount, price))

@when(u"I buy {amount:n} {anything_else:w}")
def step_when_I_buy_anything_else(context, amount, anything_else):
    price = context.shop.calculate_price_for(anything_else, amount)
    context.shopping_cart.append((anything_else, amount, price))