In this series of five articles, I want to help you get started with using SpecFlow in your test automation project. In this final chapter, we’re going to take a look at how you can effectively work with data tables in SpecFlow in order to work with more complex data structures as part of your Gherkin steps.
Tutorial Chapters
- BDD, SpecFlow and The SpecFlow Ecosystem (Chapter 1)
- Getting Started with SpecFlow (Chapter 2)
- Writing More Expressive SpecFlow Steps (Chapter 3)
- Tidying Up Your SpecFlow Features and Scenarios (Chapter 4)
- You’re here → Working with SpecFlow Tables and SpecFlow.Assist (Chapter 5)
In this fifth and final article in the series, you’ll see how to work effectively with tables in SpecFlow, and how you can easily use SpecFlow.Assist to convert tables in scenarios to C# objects as well as compare object properties to values in a table.
Tables play an important role in SpecFlow and in Gherkin scenarios in general. Not only can we specify examples for Scenario Outlines in a tabular format, but sometimes steps in SpecFlow scenarios can take parameters that are more complex than a singular value.
As an example, let’s assume that you would like to check that a call to our Zippopotam.us API returns correct data for the German zip code 24848. In the German zip code system, a zip code can be associated with more than a single place. In this case, there are four, as expressed in the following SpecFlow scenario:
Scenario: Country code de and zip code 24848 yields the expected places Given the country code de and zip code 24848 When I request the locations corresponding to these codes Then the response contains the following places | PlaceName | Longitude | Latitude | | Alt Bennebek | 9.4333 | 54.3833 | | Klein Rheide | 9.4833 | 54.45 | | Kropp | 9.5087 | 54.4111 | | Klein Bennebek | 9.45 | 54.4 |
While we could technically write an intricate method that compares each column value with the associated field for an entry in the list of places in the response returned by the GET call to http://api.zippopotam.us/de/24848, SpecFlow makes our life a lot easier by offering SpecFlow.Assist helpers. These helpers allow you to convert tables in scenarios to objects, as well as (and that’s what we’re looking for in this case) compare a collection of objects to entries in a table.
Note that there are other properties that are associated with a place, being the State and StateAbbreviation properties. These are the same (Schleswig-Holstein and SH, respectively) for all places corresponding to the German zip code 24848, but I chose not to include and verify them here.
Before we can use SpecFlow.Assist, we need to define a C# object representation of the API response. This allows us to deserialize the JSON document received from the API into an actual C# object, which is much easier to work with and perform operations on than JSON (or XML) documents.
The C# object (also known as a POCO or Plain Old C# Object) into which we will deserialize the response looks like this:
public class LocationResponse { [JsonProperty("post code")] public string PostCode { get; set; } [JsonProperty("country")] public string Country { get; set; } [JsonProperty("country abbreviation")] public string CountryAbbreviation { get; set; } [JsonProperty("places")] public List<Place> Places { get; set; } }
and each Place
in the list of places returned is transformed into this object:
public class Place { [JsonProperty("place name")] public string PlaceName { get; set; } [JsonProperty("longitude")] public string Longitude { get; set; } [JsonProperty("state")] public string State { get; set; } [JsonProperty("state abbreviation")] public string StateAbbreviation { get; set; } [JsonProperty("latitude")] public string Latitude { get; set; } }
The JsonProperty
attributes are used to help SpecFlow.Assist determine which element in the API response maps to which property. This is especially useful here, since the API response contains fields that have spaces in their names.
When we receive a response from the API, first we deserialize (transform) that into an object of type LocationResponse
:
LocationResponse locationResponse =
new JsonDeserializer().
Deserialize<LocationResponse>(response);
We now have an object with property values that correspond to the element values from the actual API response. For example, the JSON response contains a field
“country”: “Germany”
which is deserialized into the property Country
of the LocationResponse
object, assigning it the value Germany
.
This is done for all fields in the LocationResponse
object, and as such we end up with a Places
property that has as its value a list of Place
objects, one for every place in the JSON response returned by the API.
Now, we would like to compare these objects to the values specified in the
Then the response contains the following places
step in the scenario. This is where SpecFlow.Assist makes your life a whole lot easier. Instead of writing an intricate method that compares entries in the table to a specific property of individual objects in the list of Places
, we can do this:
[Then(@"the response contains the following places")] public void ThenTheResponseContainsTheFollowingPlaces(Table table) { LocationResponse locationResponse = new JsonDeserializer(). Deserialize<LocationResponse>(response); table.CompareToSet<Place>(locationResponse.Places); }
The entire table argument of the step in the SpecFlow scenario is passed to the step definition as a Table
object. The SpecFlow.Assist method CompareToSet<>()
then checks for every row (every place) in the table whether there’s a corresponding entry in the list of places returned by the API, where ‘a corresponding entry’ is defined as a place that contains the specified value for all properties (all columns) in that row.
Since this scenario passes, there’s not a lot to see. Let’s make some changes to the table to see what exactly it is that SpecFlow.Assist does here.
Change an expected property value in the table
If, for example, we change the place name Kropp into Krapp, SpecFlow.Assist will tell us that it spotted a difference:
Add a row to the table
If we add a row to the table, representing a place that is not present in the API response, SpecFlow.Assist will tell us that the expected place was not found:
Remove a row from the table
If we remove a row from the table, representing that a place was present in the response that we did not expect, SpecFlow.Assist will tell us that as well:
Change the row order in the table
If we reorder the rows in the table, SpecFlow.Assist will not tell us. This is actually a good thing as far as I’m concerned. Unless there’s an actual indication of the order in the API response, tests should not rely on the order in which items appear in a list or collection, only that they do.
If, instead of working with a collection (a List) of objects, you want to convert a single table row into a single object, as in the following example:
Scenario: Country code us and zip code 90210 yields the expected place Given the country code us and zip code 90210 When I request the locations corresponding to these codes Then the response contains the following place | PlaceName | Longitude | Latitude | State | StateAbbreviation | | Beverly Hills | -118.4065 | 34.0901 | California | CA |
Then, instead of using CompareToSet<>()
you can use CompareToInstance<>()
to directly compare the table to an object:
[Then(@"the response contains the following place")] public void ThenTheResponseContainsTheFollowingPlace(Table table) { LocationResponse locationResponse = new JsonDeserializer(). Deserialize<LocationResponse>(response); table.CompareToInstance<Place>(locationResponse.Places[0]); }
Next to comparing tables in Then steps to objects, SpecFlow.Assist also provides methods to convert tables into POCO objects, which can be very powerful when you want to specify API request bodies or other objects that you need in the setup (Given) or execution (When) phase of your scenario. The following method will convert a single-row Table
object table
into an object of type LocationRequest
:
LocationRequest locationRequest = table.CreateInstance<LocationRequest>();
In the same vein, you can create a collection (a List) of LocationRequest
objects from a multi-row Table
table
using
List<LocationRequest> locationRequests = table.CreateSet<LocationRequest>();
More examples on how to use these SpecFlow.Assist helpers can be found on the SpecFlow.Assist documentation page.
The example project used in this article can be found on GitHub: https://github.com/basdijkstra/testproject-specflow.