0

Assume I have a step defined as follows:

 Then I would expect to see the following distribution for Ford
      | engine | doors | color |
      | 2.1L   | 4     | red   |

And I have the step implementation that reads the table and does the assert as follows:

@then('I would expect to see the following distribution for {car_type}')
def step(context, car_type):
    car = find_car_method(car_type)
    for row in context.table:
        for heading in row.headings:
            assertEqual(getattr(car, heading), 
                        row[heading], 
                        "%s does not match. " % heading + \
                        "Found %s" % getattr(car, heading))

(I do it like this as this approach allows for adding more fields, but keeping it generic enough for many uses of checking attributes of the car).

When my car object has 4 doors (as an int), it does not match as data table requires that there are '4' doors (as a unicode str).

I could implement this method to check the name of the column and handle it differently for different fields, but then maintenance becomes harder when adding a new field as there is one more place to add it. I would prefer specifying it in the step data table instead. Something like:

 Then I would expect to see the following distribution for Ford
      | engine | doors:int | color |
      | 2.1L   | 4         | red   |

Is there something similar that I can use to achieve this (as this does not work)?

Note that I have cases where I need to create from the data table where I have the same problem. This make is useless trying to use the type of the 'car' object to determine the type as it is None in that case.

Thank you,

Baire

Baire
  • 45
  • 9

1 Answers1

1

After some digging I was not able to find anything, so decided to implement my own solution. I am Posting it here as it might help someone in the future.

I created a helper method:

def convert_to_type(full_field_name, value):
    """ Converts the value from a behave table into its correct type based on the name
        of the column (header).  If it is wrapped in a convert method, then use it to
        determine the value type the column should contain.

        Returns: a tuple with the newly converted value and the name of the field (without the
                 convertion method specified).  E.g. int(size) will return size as the new field
                 name and the value will be converted to an int and returned.
    """
    field_name = full_field_name.strip()
    matchers = [(re.compile('int\((.*)\)'), lambda val: int(val)),
                (re.compile('float\((.*)\)'), lambda val: float(val)),
                (re.compile('date\((.*)\)'), lambda val: datetime.datetime.strptime(val, '%Y-%m-%d'))]
    for (matcher, func) in matchers:
        matched = matcher.match(field_name)
        if matched:
            return (func(value), matched.group(1))
    return (value, full_field_name)

I could then set up by scenario as follows:

Then I would expect to see the following distribution for Ford
  | engine | int(doors) | color |
  | 2.1L   | 4          | red   |

I then changed by step as follows:

@then('I would expect to see the following distribution for {car_type}')
def step(context, car_type):
    car = find_car_method(car_type)
    for row in context.table:
        for heading in row.headings:
            (value, field_name) = convert_to_type(heading, row[heading])
            assertEqual(getattr(car, field_name), 
                        value, 
                        "%s does not match. " % field_name + \
                        "Found %s" % getattr(car, field_name))

It should be each to move 'matchers' to the module level as they don't have to be re-created each time the method is called. It is also easy to extend it for more conversion methods (e.g. liter() and cc() for parsing engine size while also converting to a standard unit).

Baire
  • 45
  • 9