Improve your SpecFlow scenarios with custom step argument transformations
One of the great benefits of using SpecFlow is that it allows you to write your specifications in a human readable format. You write your specifications as a series of free-text steps and use step definitions to make the connection between the free-text and your code that executes the step.
The step definition contains a regular expression, and the match groups within the expression define the parameters for step method, so the step definition can be reused. SpecFlow will, by default, try to do a conversion between the text in the match group and the parameter type you specify in your step method
For instance, the following step definition will match anything between "for" and "hours" that it can convert to an int
using Convert.ChangeType()
The first problem with this is that the parameter we really want is a TimeSpan
, not an int
. It would be better if we could specify TimeSpan
as the parameter type - and we can using a useful SpecFlow feature called step argument transformations.
Step argument transformations can be used to apply a custom transformation step to your parameters. You define a transformation and SpecFlow will apply it to your argument if: a) the return type of the transformation matches the parameter type, and b) the regular expression in the transformation matches the original argument. Each matching group in your regular expression will be matched, similar to how the parameters in the step definition were matched originally.
The following step transformation will match any number of digits, followed by "hour" or "hours" (we use a non-matching, optional group for the "s", so that we match both "5 hours" and "1 hour", but only capture the digit as an argument).
This step argument transformation is reusable for any step definitions, and means we can change our original step definition now use a TimeSpan
as the parameter.
This is good - but we can still only specify the time span in terms of hours. We can do better by using a smarter regular expression in our step argument transformation, taking advantage of multiple matching groups.
Now the regular expression looks a little complicated but all it's doing is allowing us to match the following formats, using matching groups for the digits we want to capture as the number of days, hours, minutes and seconds - and non-matching, optional groups for the other parts:
1 day, 1 hour, 1 minute, 1 second 5 days, 4 hours, 3 minutes, 2 seconds 5 days, 4 hours, 3 minutes 5 days, 3 minutes, 2 seconds 5 days, 4 hours, 2 seconds 4 hours, 3 minutes, 2 seconds 5 days, 4 hours 5 days, 3 minutes 5 days, 2 seconds 4 hours, 3 minutes 4 hours, 2 seconds 3 minutes, 2 seconds 5 days 4 hours 3 minutes 2 seconds
The reason for using string
rather than int
as the parameter types in the transformation and using int.TryParse()
is that SpecFlow will pass in an string.Empty
for empty matches, and it cannot convert string.Empty
into an int
.
Creating transformations like these help a lot for re-usability and you will find that the more of them you collect for your common uses, the less code you have to write in your step definitions and writing tests becomes more focused on the behaviour.
If you want to use this TimeSpan transformation, it is available as part of Endjin.SpecFlow, which contains a bunch of other useful shared steps (and you can read more about it here).