Advanced SpecFlow: Custom Conversion of Step Parameters and Data Tables with [StepArgumentTransformation]

SpecFlow is a tool that allows the writing of business-readable tests that can then be automated in code. If you’re new to SpecFlow check out my Pluralsight course or the resources page to get up to speed before looking at these more advanced topics.

SpecFlow will attempt to perform some conversions of step arguments as specified in the documentation.

We can also tell SpecFlow to use our own custom conversions. Take the feature below:

Feature: SomeFeature

Scenario: Update User Password
    Given The original user was created over 10 days ago
    When I update the users password to "some password"
    Then I should see the following errors
        | ErrorCode         | ErrorDescription          |
        | 442               | Out of date user          |
        | 28                | Changes were not saved    |

From this we generate some basic step definitions:

[Given]
public void Given_The_original_user_was_created_over_DAYS_days_ago(int days)
{
}

[When]
public void When_I_update_the_users_password_to_NEWPASSWORD(string newPassword)
{
}
       
[Then]
public void Then_I_should_see_the_following_errors(Table table)
{            
}

Automatically Parsing 10 Days Ago into a DateTime

In the Given step we get an int specifying how many days ago the user was created. As it’s the Given step we need to put the system into a state where the user creation date, for example held as a DateTime in the user database, is 10 days old. If this is just a single step then we can simply write the code to subtract a number of days from todays date in the step itself. Note: this kind of date based testing where we rely on “now” may prove brittle, imagine the a Given ran just before midnight, but the Then gets executed after midnight, i.e. the next day.

If we have a lot of instances where we use “n days ago” throughout our scenarios, we can tell SpecFlow to perform a custom conversion instead.

In the scenario above, we want to treat “10 days ago” as something special.

First we need to change the given step definition to the regex style (underscore style doesn’t seem to work at present with custom parameter conversion):

[Given(@"The original user was created over (.*) days ago")]
public void GivenTheOriginalUserWasCreatedOverDaysAgo(int p0)
{
}

Next we modify this to capture the entire phrase, not just the number (10) but the whole phrase: “10 days ago”:

[Given(@"The original user was created over (.* days ago)")]
public void GivenTheOriginalUserWasCreatedOverDaysAgo(int p0)
{
}

Now we’re capturing the whole phrase, we change the parameter to a DateTime:

[Given(@"The original user was created over (.* days ago)")]
public void GivenTheOriginalUserWasCreatedOverDaysAgo(DateTime userCreatedDate)
{
}

If we try to run the scenario now we get and error: “System.FormatException : The string was not recognized as a valid DateTime. There is an unknown word starting at index 3”. This is because SpecFlow is trying to perform an invalid conversion from the string “10 days ago” to a DateTime.

The final step is to tell SpecFlow to perform a custom conversion when it sees “10 days ago” (or any number of days ago) and needs to transform it into a DateTime instance.

To do this we use the [StepArgumentTransformation] attribute. We can use this on a method in the step definition class itself, or create a new class with the [Binding] attribute applied:

[Binding]
public class CustomTransforms
{
    [StepArgumentTransformation(@"(\d+) days ago")]
    public DateTime DaysAgoTransform(int daysAgo)
    {
        return DateTime.Today.AddDays(-daysAgo);
    }
}

If we run the tests now they will execute as expected, and the value that SpecFlow passes to our Given step parameter “userCreatedDate” is a valid DateTime of ten days ago.

The [StepArgumentTransformation] attribute can be used without a regex parameter (to match anything), but in this example we’re capturing the digits “(/d+)” representing the number of days ago.

It’s important (and potentially confusing at first) to note that the regex in the [StepArgumentTransformation] is matching the entire string that we first captured in the given, i.e. “10 days ago”. From this initially captured string “10 days ago”, we drill down into the actual numeric information that we want, i.e. “10”. These captured digits “10” are then passed as an int to the daysAgo parameter for use in our conversion method DaysAgoTransform which returns a DateTime that SpecFlow passes on to our Given step parameter.

Converting a Table to a Strongly Typed Collection

For the Then step:

[Then]
public void Then_I_should_see_the_following_errors(Table table)
{            
}
SpecFlow passes us a weakly typed Table object contain the rows/columns.

We can use the [StepArgumentTransformation] to define a translation to s strongly typed collection of Error objects:

public class Error
{
    public int ErrorCode { get; set; }
    public string ErrorDescription { get; set; }
}

We now change the Then to take a list of these Error objects:

[Then]
public void Then_I_should_see_the_following_errors(IEnumerable<Error> errors)
{
}

Finally we tell SpecFlow about our desired custom conversion:

[StepArgumentTransformation]
public IEnumerable<Error> ErrorsTransform(Table table)
{
    return table.CreateSet<Error>();
}

This code uses the CreateSet method from the SpecFlow Assist Helpers to convert each row in the Table object into an Error instance which results in:

image

To learn more about step parameter conversion, check out my SpecFlow Tips and Tricks Pluralsight course.

Comments (7) -

  • Logan

    12/9/2013 7:00:32 AM | Reply

    I have not used Step Arguement Transformation yet in my projects, though I have dealt with above mentioned examples. I simply transform input into the required types in the step definition itself. I'll try and use the above in forthcoming projects. Good stuff.

  • Jason

    12/9/2013 9:52:04 AM | Reply

    Hi Logan, thanks - let me know how you get on Smile

  • Marcus Hammarberg

    12/9/2013 7:22:59 PM | Reply

    Hi Jason,

    thanks for all your awesome posts and the great course on SpecFlow. I've been following and supporting the tool from the very (oh well 0.3 or something) versions.

    Your readers that are comfortable using dynamics can save a lot of typing by using the SpecFlow.Assist.Dynamic package (marcusoftnet.github.io/SpecFlow.Assist.Dynamic/).

    In this package you'll find methods that transforms tables into dynamic objects, and hence you don't need the typed classes. There's quite a lot of times that I've ended up with a lot of classes that only is used by my specs.

    Keep the great work up!

  • Jason

    12/10/2013 8:16:45 AM | Reply

    Hi Marcus, thanks - will check out assist.dynamic - cheers, Jason

  • Jason

    2/14/2014 3:08:45 PM | Reply

    I just released new Pluralsight course "SpecFlow Tips and Tricks" that covers this topic (and other useful things) : http://bit.ly/psspecflowtips

  • Mahendra

    7/28/2017 1:27:50 PM | Reply

    Thanks for the very good courses on spec flow ,

    Can we specify order to feature file to run on order basis.

    • Jason Roberts

      8/14/2017 7:53:40 AM | Reply

      Hi Mahendra, there has already been some discussion of this on the course discussion board, feel free to check it out at: http://disq.us/p/1hcu5iw

Add comment

Loading