Joe Buschmann

let topics = [csharp; specflow; fun]

More Specflow Tips

UPDATE (11/18/2016):

I've written a number of posts since this one was published that cover advanced Specflow topics like composable steps, tags done right, managing state, useful regex, etc. For a list of all my Specflow writing, you can click on the Specflow tag or go here: http://joebuschmann.com/tag/specflow. Specflow is a fantastic tool that's changed the way I develop software, and I hope it does the same for you. Happy testing!


My last post covered three tips or best practices for Specflow that covered manipulating the current ScenarioContext with extension methods, binding multiple Given/When/Then attributes, and using hooks. In this post I’ll be covering three more tips for working with Specflow tables.

Specflow tables provide an easy way of specifying the data for a collection or an individual object. The table in the specification step is passed into the step definition as an instance of the TechTalk.SpecFlow.Table class.

When specifying a collection of data, each table row represents an item in the collection. The table headers indicate to which property the column values should be assigned. The example below defines a list of items with each item having an Name  and a Price property.

Given the user orders the following items
    | Name                                                           | Price |
    | The Mythical Man-Month                                         | 29.09 |
    | The Phoenix Project                                            | 29.95 |
    | Design Patterns: Elements of Reusable Object-Oriented Software | 39.95 |
    | Lean In: Women, Work, and the Will to Lead                     | 11.22 |

Another use for a table is to specify the values of properties for a single instance of an object. For example the FirstName, LastName, and Email properties of a customer object could be listed with a name/value pair for each property.

Given the following customer
    | Name      | Value             |
    | FirstName | Lucy              |
    | LastName  | Buschmann         |
    | Email     | [email protected] |

Generally, I’ve found defining tables in specification steps to be straightforward. The more tedious part is translating the table data in the step definitions.

Avoid Explicitly Enumerating Table Rows

I try to avoid enumerating table rows in code as much as possible. Fortunately, there are helper methods in the TechTalk.SpecFlow.Assist.TableHelperExtensionMethods class that make translating from a table instance to a collection or object much easier. I’ve found very few instances where these methods didn’t give me what I needed.

The following step definitions explicitly enumerate a table to retrieve data. The first one builds out a list of items in a shopping cart and the second, a customer instance. They correspond to the step specifications in the previous section.

[Given(@"the user orders the following items")]  
public void GivenTheUserOrdersTheFollowingItems(Table table)  
{
    var shoppingCartItems = new List<ShoppingCartItem>();

    foreach (var row in table.Rows)
        shoppingCartItems.Add(new ShoppingCartItem { Name = row["Item"], Price = Convert.ToDecimal(row["Price"]) });

    ScenarioContext.Current.ShoppingCartItems(shoppingCartItems);
}

[Given(@"the following customer")]
public void GivenTheFollowingCustomer(Table table)  
{
    var customer = new Customer
        {
            FirstName = table.Rows.First(r => r["Name"]  "FirstName")["Value"],
            LastName = table.Rows.First(r => r["Name"]  "LastName")["Value"],
            Email = table.Rows.First(r => r["Name"]  "Email")["Value"]
        };

    ScenarioContext.Current.Customer(customer);
}

This code cleans up nicely with calls to table.CreateSet() and table.CreateInstance(). The number of lines drops to two per step.

[Given(@"the user orders the following items")]  
public void GivenTheUserOrdersTheFollowingItems(Table table)  
{
    var shoppingCartItems = table.CreateSet<ShoppingCartItem>().ToList();
    ScenarioContext.Current.ShoppingCartItems(shoppingCartItems);
}

[Given(@"the following customer")]
public void GivenTheFollowingCustomer(Table table)  
{
    var customer = table.CreateInstance<Customer>();
    ScenarioContext.Current.Customer(customer);
}

Convert Table Data to Anonymous Types for Ease of Use

The extension methods in TechTalk.SpecFlow.Assist.TableHelperExtensionMethods are useful for simple one-to-one mappings between table values and class properties; however they cannot be used for more complex mappings which require manual intervention. Let’s say the shopping cart in the previous examples is enhanced to include discounts as child items under the corresponding product. This makes the ShoppingCartItem class hierarchal and difficult to model in a flat table structure. A new column called ParentItem has been added to the specification to define the parent-child relationships, but it doesn’t map easily to the new list of child items in the ShoppingCartItem class. This means table.CreateSet() can’t be used.

public class ShoppingCartItem  
{
    public ShoppingCartItem()
    {
        ShoppingCartItems = new List<ShoppingCartItem>();
    }

    public string Name { get; set; }

    public decimal Price { get; set; }

    /// <summary>
    /// A list of child shopping cart items.
    /// </summary>
    public List<ShoppingCartItem> ShoppingCartItems { get; set; }
}
Given the user orders the following items
    | Name                                                           | ParentItem          | Price  |
    | The Mythical Man-Month                                         |                     | 29.09  |
    | The Phoenix Project                                            |                     | 29.95  |
    | Super Saver Discount                                           | The Phoenix Project | -10.00 |
    | Promotional Discount                                           | The Phoenix Project | -6.00  |
    | Design Patterns: Elements of Reusable Object-Oriented Software |                     | 39.95  |
    | Lean In: Women, Work, and the Will to Lead                     |                     | 11.22  |

With this new requirement, the step definition becomes:

[Given(@"the user orders the following items")]  
public void GivenTheUserOrdersTheFollowingItems(Table table)  
{
    var shoppingCartItems = new List<ShoppingCartItem>();

    foreach (var row in table.Rows)
    {
        var shoppingCartItem = new ShoppingCartItem { Name = row["Name"], Price = Convert.ToDecimal(row["Price"]) };

        if (string.IsNullOrEmpty(row["ParentItem"]))
        {
            shoppingCartItems.Add(shoppingCartItem);
        }
        else
        {
            shoppingCartItem.Name = "> " + shoppingCartItem.Name;
            var parentShoppingCartItem = shoppingCartItems.First(s => s.Name  row["ParentItem"]);
            parentShoppingCartItem.ShoppingCartItems.Add(shoppingCartItem);
        }
    }

    ScenarioContext.Current.ShoppingCartItems(shoppingCartItems);
}

This step definition works, but I don’t like now the row indexer is used throughout the method. I think this makes the code less readable.

As an alternative, the table could be converted to a list of objects up front. Anonymous types let us avoid having to define a class for such a simple and transient use case, and the remaining code uses the anonymous type which keeps things neat.

[Given(@"the user orders the following items")]  
public void GivenTheUserOrdersTheFollowingItems(Table table)  
{
    // Use an anonymous type up front to avoid having to use the table
    // in the remainder of the method.
    var items =
        table.Rows.Select(
            r => new {Name = r["Name"], ParentItem = r["ParentItem"], Price = Convert.ToDecimal(r["Price"])});

    // The rest of the method doesn't use the table or row indexers.
    var shoppingCartItems = new List<ShoppingCartItem>();

    foreach (var item in items)
    {
        var shoppingCartItem = new ShoppingCartItem {Name = item.Name, Price = item.Price};

        if (string.IsNullOrEmpty(item.ParentItem))
        {
            shoppingCartItems.Add(shoppingCartItem);
        }
        else
        {
            shoppingCartItem.Name = "> " + shoppingCartItem.Name;
            var parentShoppingCartItem = shoppingCartItems.First(s => s.Name == item.ParentItem);
            parentShoppingCartItem.ShoppingCartItems.Add(shoppingCartItem);
        }
    }

    ScenarioContext.Current.ShoppingCartItems(shoppingCartItems);
}

Flatten Object Hierarchies When Doing Table Comparisons

Now we have steps for reading in order items and saving them as a list of ShoppingCartItem instances in the ScenarioContext. Subsequent steps can manipulate the order which in turn updates the shopping cart. It needs to be validated at the end of the test to ensure correct behavior.

Then the following items should appear in the shopping cart
    | Name                                                           | Price  |
    | The Mythical Man-Month                                         | 29.09  |
    | The Phoenix Project                                            | 29.95  |
    | > Super Saver Discount                                         | -10.00 |
    | > Promotional Discount                                         | -6.00  |
    | Design Patterns: Elements of Reusable Object-Oriented Software | 39.95  |
    | Lean In: Women, Work, and the Will to Lead                     | 11.22  |

The flat list of expected shopping cart items needs to be validated against the hierarchal shopping cart. The step definition below does just that, but it violates tip one which is to avoid explicitly enumerating table rows. It is also difficult to follow since it needs to iterate through the child shopping cart items.

[Then(@"the following items should appear in the shopping cart")]  
public void ThenTheFollowingItemsShouldAppearInTheShoppingCart(Table table)  
{
    var shoppingCartItems = ScenarioContext.Current.ShoppingCartItems().ToList();
    shoppingCartItems.Reverse();
    var stack = new Stack<ShoppingCartItem>(shoppingCartItems);

    for (int i = 0; i < table.Rows.Count; i++ )
    {
        var row = table.Rows[i];
        var shoppingCartItem = stack.Pop();

        Assert.That(row["Name"], Is.EqualTo(shoppingCartItem.Name));
        Assert.That(Convert.ToDecimal(row["Price"]), Is.EqualTo(shoppingCartItem.Price));

        var childItems = shoppingCartItem.ShoppingCartItems.ToList();
        childItems.Reverse();

        foreach (var childShoppingCartItem in childItems)
            stack.Push(childShoppingCartItem);
    }
}

Ideally we would like to use the helper extension methods to do the table comparison. This is possible by first flattening out the shopping cart hierarchy into a list. The Flatten() extension method does the trick and is invoked right before table.CompareToSet(). Now the step is just two lines of code. Much better.

public static class ShoppingCartExtensions  
{
    /// <summary>
    /// Returns each item in the shopping cart hierarchy in a simple list.
    /// </summary>
    public static List<ShoppingCartItem> Flatten(this List<ShoppingCartItem> shoppingCartItems)
    {
        var allItems = new List<ShoppingCartItem>();
        shoppingCartItems.Flatten(allItems);
        return allItems;
    }

    private static void Flatten(this List<ShoppingCartItem> shoppingCartItems, List<ShoppingCartItem> allItems)
    {
        shoppingCartItems.ForEach(i =>
            {
                allItems.Add(i);
                i.ShoppingCartItems.Flatten(allItems);
            });
    }
}

[Then(@"the following items should appear in the shopping cart")]
public void ThenTheFollowingItemsShouldAppearInTheShoppingCart(Table table)  
{
    var shoppingCartItems = ScenarioContext.Current.ShoppingCartItems().Flatten();
    table.CompareToSet(shoppingCartItems);
}

That’s all I have for now. For more information on tables and the table helper methods, check out the links below.

Specflow Assist Helpers

Full Specflow Documentation