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.
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.
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.
This code cleans up nicely with calls to table.CreateSet() and table.CreateInstance(). The number of lines drops to two per step.
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.
With this new requirement, the step definition becomes:
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.
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.
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.
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.
That’s all I have for now. For more information on tables and the table helper methods, check out the links below.