Data tables
If you look at the Multiple shouters scenario that includes Sean, Lucy, and Oscar you can see that it’s a bit hard to read. SpecFlow and Gherkin let you use data tables in a step, which can help you remove clutter and improve the readability of the specification. The scenario could be simplified by using a data table, like this:
Given people are located at
| Name | X | Y |
| Lucy | 0 | 0 |
| Sean | 0 | 500 |
| Oscar | 1100 | 0 |
Modify the scenario and run SpecFlow. You should get an undefined step error, because this is a new step. Look at the snippet that has been emitted, which defines a step definition that takes a parameter .
- C#
- JavaScript
- Go
There are a number of ways that you can use this parameter, but to start with let’s keep it simple and use the Table.Rows property, which returns a collection of objects that implement the IDictionary<string, string> interface. There’s an entry in the collection for each row (except the heading).
You can use this in your step definition to see an example.
foreach (var row in table.Rows)
{
// access cells by e.g. row["name"]
}
Cucumber will generate a snippet where the step definition takes a DataTable argument. That table object lets you iterate over the rows and get each row as a simple JavaScript object keyed by the header names.
You can see what’s inside it with something like:
Given('people are located at', function (dataTable) {
dataTable.hashes().forEach(row => {
console.log(row)
})
})
Each row is an object like:
{ Name: 'Lucy', X: '0', Y: '0' }
You can then use these values to set up your Shouty locations.
There are a number of ways that you can use this parameter, but to start with let’s keep it simple and use the Table.Rows property, which returns a slice of pointers to TableRow structs. There’s an entry in the slice for each row. Each row has a Cells property that returns a slice of pointers to TableCell structs. Each cell has a Value property that returns the value of the cell.
You can use this in your step definition to see an example.
// skip the first row, which contains the header
for _, row := range table.Rows[1:] {
// access cells by e.g. row.Cells[0].Value
}
How much do you like the look of the code?
- C#
- JavaScript
- Go
Next, create a new class PersonLocation (in the Shouty.Specs project) with public properties called
Name, X, and Y.
Then, change the step definition parameter from Table to PersonLocation[].
When you run SpecFlow you should get an InvalidCastException exception because SpecFlow doesn’t know how to convert the data table to a PersonLocation array. Add the following to the ShoutStepDefinitions class:
// ...
[StepArgumentTransformation]
public PersonLocation[] ConvertPersonLocations(Table personLocationsTable)
{
return personLocationsTable.Rows
.Select(row => new PersonLocation
{
Name = row["name"],
X = int.Parse(row["x"]),
Y = int.Parse(row["y"])
}).ToArray();
}
In JavaScript we don’t need an extra helper type — the combination of table.hashes() and plain objects is usually enough.
Update your step definition to convert each row into a call to setLocation:
Given('people are located at', function (dataTable) {
dataTable.hashes().forEach(row => {
const name = row.Name
const x = parseInt(row.X, 10)
const y = parseInt(row.Y, 10)
shouty.setLocation(name, new Coordinate(x, y))
})
})
This keeps the Gherkin readable while keeping the step code straightforward and idiomatic for JavaScript.
Next, create a new struct PersonLocation with properties called Name, X, and Y.
Then install the Go package github.com/rdumont/assistdog and change the step definition implementation to use assistdog to convert the data table to a PersonLocation slice.
func peopleAreLocatedAt(table *godog.Table) {
assist := assistdog.NewDefault()
locations, _ := assist.CreateSlice(new(PersonLocation), table)
// ...
}
Change the sample code above to iterate over the list.
Is the code more readable?
Now get the scenario passing again.
The "Assist" table helper functions
- C#
- JavaScript
- Go
The Rows property is very useful, but included in the TechTalk.SpecFlow.Assist namespace are some extension methods of Table that provide some further functionality. The SpecFlow.Assist helpers docs are helpful here.
CreateSet<T>()— This method will create an instance ofTfor each non-header row of the table. It will try to match the row headers with the names of public properties ofTand set them using the values found in each row.CreateInstance<T>()— Works similarly toCreateSet, but creates a single instance from data tables that have either a single data row or field and value columns and a row for each field setting.CompareToSet<T>(IEnumerable<T>, bool)andCompareToInstance<T>(T)— These helper methods can be used inThensteps to assert that a list ofTobjects contain the property values that were specified in the data table.
Change your implementation of the ConvertPersonLocations method to use the CreateSet method. Don’t forget to add a namespace using to the TechTalk.SpecFlow.Assist namespace.
Cucumber.js doesn’t provide a built-in “Assist” library like other language implementations.
Instead, the DataTable object gives you a handful of simple but powerful helpers that cover most common use cases.
Useful Cucumber.js DataTable methods
-
table.hashes()
Returns an array of objects using the header row as keys.
Ideal for rows shaped like:Name X Y const rows = table.hashes()
// → [ { Name: 'Lucy', X: '0', Y: '0' }, ... ] -
table.rows()Returns an array of arrays for all non-header rows. -
table.raw()Returns the entire table (including header) as an array of arrays. -
table.transpose()Swaps rows and columns—useful when the table is vertical instead of horizontal.
While the Rows field is quite useful for working with data tables AssistDog offers utilities that simplify the conversion and comparison of Gherkin table data with Go data structures. The AssistDog api documentation and examples are helpful here.