Covert Step Parameters into Useable Types with Step Argument Transformations

2021-10-08 21-10-08
Gherkin

Share!

 

In this post we will look at how parameter strings in Gherkin steps are converted to arguments in step binding methods, and how we can implement our own custom conversions using step argument transformations.

The Basics

From Gherkin to Code

When writing Gherkin style specs, you typically want to reuse your steps with slight variations. Gherkin supports parameterized steps to do so.

In its simplest form, the value for parameters can be specified inline in a scenario:

Scenario: Ordering an item in stock
  Given we have '3' items in stock.
  When we order '1' item
  Then we should have '2' items in stock left.

Without any modifications to the bindings you could use the same steps in other scenarios:

Scenario: Ordering the last item in stock
  Given we have '1' items in stock.
  When we order '1' item
  Then we should have '0' items in stock left.

Within a Scenario Outline, you can simply provide multiple values for each parameter in the Example section:

Scenario Outline: Ordering items in stock
  Given we have '<ItemsInStock>' items in stock.
  When we order '<OrderedItems>' item
  Then we should have '<ItemsLeft>' items in stock left.
Examples:
| ItemsInStock | OrderedItems | ItemsLeft |
|            3 |            1 |         2 |
|            1 |            1 |         0 |

The binding for the Given step could look like this:

[Given(@"we have '(.*)' items in stock\.")]
public void GivenWeHaveASpecificNumberOfItemsInStock(int itemsInStock)
{
   // ... set up the stock
}

What is happening behind the scenes?

In order for this to work, SpecFlow must convert the string parameter coming from the step to an instance of the datatype used in the binding method. In our case, this conversion is from a string to an int:

SpecFlow provides out-of-the-box support for the conversion of most primitive datatypes (see Standard Conversion in the SpecFlow documentation). This even includes support for converting enum members to the proper type.

Wouldn’t it be cool …

The whole idea behind writing specifications in Gherkin is to improve collaboration between all members of an agile team. You will therefore want to put significant effort into formulating specifications that are easily understood by all stakeholders. After you have discussed a specification and drafted a version of it in Gherkin, it is a good idea to read it out loud again and ask if this is how you would express yourself when explaining it to someone else.

While the line

Given we have '0' items in stock.

might be technically correct, you probably wouldn’t talk like this to another person. In human-to-human communication you would probably instead say something like

Given we have no items in stock.

Of course, this will not work with the existing step binding, as there is no standard conversion from the string “no” to an integer (in this case zero). To overcome this, we could go back to our string argument and handle this special case ourselves:

[Given("we '(.*)' items in stock\.")]
public void GivenWeHaveASpecificNumberOfItemsInStock(string itemsInStockExpression)
{
   var itemsInStock = (itemsInStockExpression == "no")
                    ? 0
                    : int.Parse(itemsInStockExpression);
   // ... set up the stock
}

While this approach works, it has a few issues:

1) Feeling pain for wanting to be nice

No matter how nice and clean the step binding was before (maybe just a single line calling the production API?), we have now mixed in some code just to make the specs nicer to read. As we want all members in our team work together (whatever their role), it should be easy for a non-coder (e.g. a tester) to look at a step binding’s code and have a good idea of what is going on. The more technical plumbing code (similar to the code above) there is, the harder it becomes to identify the important parts of that method.

To somewhat ease this problem, we can extract the conversion code to a dedicated method:

[Given("we '(.*)' items in stock\.")]
public void GivenWeHaveASpecificNumberOfItemsInStock(string itemsInStockExpression)
{
   var itemsInStock = GetNumberFromHumanString(itemsInStockExpression);
   // ... set up the stock
}

private int GetNumberFromHumanString(string itemsInStockExpression)
{
   if (itemsInStockExpression == "no")
      return 0;

   return int.Parse(itemsInStockExpression);
}

While this helps a little bit, we still are left with the call to this method and the method code itself.

2) The signature of the binding lost its expressiveness

With the original version of the step binding, we can clearly see that we expect an integer for the number of items in stock. The second version (with the string parameter) is extremely generic, as it just accepts a string. To see what string this is and what the allowed values are, you need to identify, inspect and understand the code that converts the string to the actual number. If additional cases are added, the binding can become contrived and very difficult to read over time.

Step Argument Transformations to the Rescue

In situations similar to the one described above, SpecFlow’s Step Argument Transformation feature comes in handy. Step argument transformations allow you to extend SpecFlow’s ability to convert strings in Gherkin steps to any type you wish. This means that we can go back to the very basic version of the step binding in our example, and inform SpecFlow of the desired conversion in a separate step transformation.

Here is our step binding:

[Given(@"we have '(.*)' items in stock\.")]
public void GivenWeHaveASpecificNumberOfItemsInStock(int itemsInStock)
{
   // ... set up the stock
}

Note that there is no conversion here whatsoever. The conversion is introduced by providing a step argument transformation like this:

[Binding]
public class MyStepArgumentTransformations
{
   [StepArgumentTransformation]
   public int TransformItemsInStockExpressionToInteger(string itemsInStockExpression)
   {
      if (itemsInStockExpression == "no")
         return 0;

      return int.Parse(itemsInStockExpression);
   }
}

Note that the method is marked with the StepArgumentTransformation n attribute and has to reside in a class marked with the Binding attribute. This is all that is needed for SpecFlow to use this method to convert the parameter to an int.

As this method now stands on its own, it can be used in other contexts as well. So we should remove the residuals of the initial use case by refactoring the names to something more generic:

[StepArgumentTransformation]
public int TransformHumanReadableIntegerExpression(string expression)
{
   if (expression == "no")
      return 0;

   return int.Parse(expression);
}

Instead of the conversion code being buried in the step binding or an extracted helper method, we now have the conversion separated into its own class.

The whole situation now looks like this:

This has some consequences:

  • We have returned to separated concerns. Converting the human readable expressions to what we actually need in the code (the integer in this case) can be considered a UI concern. By moving the conversion code to the implicit layer of SpecFlow magic, the binding is back on focusing on the business concern of the Gherkin step, i.e. wiring to the production code.
  • As the conversion code now stands alone, it can be tested as well. While the current implementation seems quite obvious, we might want to increase our confidence in parts of the test automation layer by testing them separately.

Restricting the considered strings

While you can overwrite an already existing conversion (string to int in our example), be aware that this conversion is available globally in all projects referencing the assembly containing the class.

For this reason, you might want to restrict the cases where SpecFlow applies this conversion. This is possible by specifying a regular expression for the transformation:

[StepArgumentTransformation("(\d+|[no])")]
public int TransformHumanReadableIntegerExpression(string expression)
{
   // ...
}

With this regex we tell SpecFlow to just consider strings consisting of at least one digit (the \d+ part) or the string “no”. This constraint has the advantage that if someone uses a very different string for such a parameter, SpecFlow’s default conversion kicks in, and its error message is displayed.

Even more Powerful: Conversions to Custom Types

As mentioned earlier, decreased readability is one disadvantage of having to convert string input to the target format in the step binding.

Using step argument transformations, the expressiveness of step bindings can be improved even further by using custom types for parameters.

For example, we could introduce a tiny type, HumanReadableIntegerExpression:

public void GivenWeHaveASpecificNumberOfItemsInStock(HumanReadableIntegerExpression itemsInStockExpression)
{
   var itemsInStock = itemsInStockExpression.Value;
   // ... set up the stock
}

Notice the following benefits:

  • The signature of the method now tells us more about the supported strings we can use in our bindings. Of course, we still have to know (or look up) the exact expressions that are supported. But as we are getting familiar with helpers like this, we immediately know what expressions we can write.
  • As the value is immediately available from the method’s parameter, we could even inline the local variable in the above example and would require no additional lines of code for improved bindings.
  • The conversion is separated out in the step argument transformation, such that a non-coder could swap one possibility to express the number by another one, without having to change cumbersome wiring. For example, she could replace HumanReadableIntegerExpression with HumanReadableIntegerExpressionAdvanced with support for more advanced expressions.

HumanReadableIntegerExpression itself is a tiny class:

public class HumanReadableIntegerExpression
{
   public int Value { get; }
   public HumanReadableIntegerExpression(int value) { Value = value; }
}

We then just need to change the transformation method to return an instance of that class:

[StepArgumentTransformation]
public HumanReadableIntegerExpression TransformHumanReadableIntegerExpression(string expression)
{
   if (expression == "no")
      return new HumanReadableIntegerExpression(0);

   return new HumanReadableIntegerExpression(int.Parse(expression));
}

Restrictions

Step argument transformations will not work with tables provided by TechTalk.SpecFlow.Assist (e.g. with the Create <T> or CreateInstance<T> methods). They can be used with parameters in example tables.

Wrapping Up

Using step argument transformations can bring a number of benefits, allowing you to write steps in a human-readable way that reflects the way you would express yourself in normal conversation. However, it is important to ensure that team members have at least a basic understanding of the “magic” going on behind the scenes.

Benefits

  • Move the code used for conversions from your own bindings to a dedicated class wired by SpecFlow.
  • Reveal the intention of the step binding method and clarify the ability to express parameters using specific values.

Pitfalls

It can be challenging for team members not familiar with the step argument transformations feature to understand how SpecFlow “magically” knows how to interpret a string such as “no” as 0. But this is generally an easy problem to solve.