logo logo

Writing More Expressive SpecFlow Steps

SpecFlow Tutorial

In this series of five articles, I want to help you get started with using SpecFlow in your test automation project. In this chapter, we’ll take a close look at creating more intelligent and flexible scenarios with the goal of creating expressive specifications that support communication about application behaviour and acceptance test goals and results.

Tutorial Chapters

  1. BDD, SpecFlow and The SpecFlow Ecosystem (Chapter 1)
  2. Getting Started with SpecFlow (Chapter 2)
  3. You’re here → Writing More Expressive SpecFlow Steps (Chapter 3)
  4. Tidying up your features and scenarios (Chapter 4)
  5. Working with SpecFlow tables and SpecFlow.Assist (Chapter 5)

In the previous article, we have seen how to set up a SpecFlow project in Visual Studio, how to add a first SpecFlow feature to the project and how to let SpecFlow auto-generate step definitions that implement the steps in various scenarios. In this article, we are going to take a closer look at how steps in SpecFlow scenarios and step definition methods work together, as well as some techniques to make your SpecFlow steps more powerful and expressive.

Matching Steps with Step Definitions

When we asked SpecFlow to generate the step definitions that implement the steps in the example feature for us in the previous article, it generated a C# source code file with step definitions in it. But how does SpecFlow know what step definition (i.e., what method) to run when it encounters a specific step? How are the scenario step and the step definition connected?

There are two factors playing a part here. First, SpecFlow will only look for step definitions in classes that have the [Binding] annotation, as shown in the snippet below:

namespace testproject_specflow.StepDefinitions
{
    [Binding]
    public class ReturningLocationDataBasedOnCountryAndZipCodeSteps
    {
        [Given(@"the country code (.*) and zip code (.*)")]
        public void GivenTheCountryCodeAndZipCode(string countryCode, string zipCode)
        {
        }

        [When(@"I request the locations corresponding to these codes")]
        public void WhenIRequestTheLocationsCorrespondingToTheseCodes()
        {
        }

        [Then(@"the response contains the place name (.*)")]
        public void ThenTheResponseContainsThePlaceName(string expectedPlaceName)
        {
        }

Second, SpecFlow matches steps with step definitions methods inside these classes annotated with [Binding] following one of three strategies. These correspond with the three style options you have when you generate step definitions in Visual Studio (see the previous article).

Regular Expressions

This is the most versatile matching strategy, and also the one you see in the example above. When selecting this style, SpecFlow adds an annotation to the step definition method that contains a regular expression that matches the step as it appears in the feature file. So, for example, the step:

Given I use regular expressions to match steps with step definitions

is implemented by a step definition method that looks like this:

[Given(@"I use regular expressions to match steps with step definitions")]
public void IUseRegularExpressionsToMatchStepsWithStepDefinitions(string countryCode, string zipCode)
{
}

The part between the () brackets in the annotation is interpreted by SpecFlow as a regular expression, and when a step in a feature file matches this regular expression, the associated method is executed. The regular expression above is straightforward, as in: it only matches a very specific step (the one you see a couple of lines above the code snippet), but we’ll see later on in this article how you can leverage the power of regular expressions to create more flexible steps and step definitions.

Note that the actual method name (IUseRegularExpressionsToMatchStepsWithStepDefinitions) is of no influence on whether or not the step matches the step definitions, i.e., the method name can be anything as long as it’s a valid C# method name. For readability purposes, though, it might be useful to give the step definition method a name similar to the actual step. This comes in especially handy when an error or exception is thrown during the execution of a scenario and you’re left with a stack trace to decipher.

Method Names – Pascal Casing

Another strategy that’s available to you when matching steps with step definitions is by using specific method names and Pascal casing, i.e., starting every new word with a capital letter.

To give you an example, when using this matching style, the step:

Given I use Pascal casing to match steps with step definitions

is matched by the following step definition:

[Given]
public void GivenIUsePascalCasingToMatchStepsWithStepDefinitions()
{
}

Pascal casing-based matching is less flexible than the regular expression matching style, which explains why it’s not used as often as the former.

Method Names – Underscores

Finally, there’s also the option to use an underscores-based matching style. When using this matching style, the step:

Given I use underscores to match steps with step definitions

is matched by this step definition:

[Given]
public void Given_I_use_underscores_to_match_steps_with_step_definitions()
{
}

The same disadvantages apply here: this matching style does not offer the power and flexibility that regular expressions do, explaining why you won’t see this used as often in practice.

 

Using Regular Expressions to Create More Flexible Steps

Let’s see some of the ways you can use regular expressions to create more flexible steps and ultimately, make your scenarios more readable and expressive.

In the example feature file you have seen in the previous article, we expressed that we were going to use the Zippopotam.us API to look up locations that correspond to the US zip code 90210. We specified these values in a Given step like this:

Given the country code us and zip code 90210

which can be matched by the step definition method

[Given(@"the country code us and zip code 90210")]
public void GivenTheCountryCodeUsAndZipCode90210()
{
}

But what if you have another scenario that uses different values for the country and/or the zip code? What if you have this step instead of the previous one?

Given the country code ca and zip code B2A

If you would simply copy and paste the existing step definition method and slightly change the regular expression used to match it with a step, you might quickly end up with a large number of step definitions that had really similar purposes and implementations. What you would not have, though, is a maintainable code base.

To handle these situations, you can use the power of regular expressions to make your steps more flexible. So, instead of copying and pasting steps that are expressed similarly, you might create a parameterized step definition instead:

[Given(@"the country code (.*) and zip code (.*)")]
public void GivenTheCountryCodeAndZipCode(string countryCode, string zipCode)
{
}

This step definition matches both 

Given the country code us and zip code 90210

and

Given the country code ca and zip code B2A

as well as steps containing any other value for the country code and zip code, because the regular expression (.*) matches strings that consist of any possible character (expressed by the dot) and of length 0 or more (expressed by the asterisk).

The parameter values are passed to the step definition method parameters countryCode (this will be assigned the value us or ca) and zipCode (which will equal 90210 or B2A). You can then use these variables in your step definition method body to make your test code more flexible and powerful, too.

If you want to avoid matching empty strings, you can replace (.*) with (.+), which matches strings with any possible character of length 1 or above (expressed by the plus sign).

When you have installed the SpecFlow Extension, Visual Studio visualizes that the country and zip code are parameterized in this regular expression by italicizing the values in the steps and giving them a different color:

 

Other Useful Regular Expression Features

While the (.*) and (.+) regular expressions are very versatile, as in they accept practically any parameter value, sometimes you might want to be a little more strict in the types of parameters you accept in your step definitions. Let’s look at some of the possibilities that regular expressions provide to do just that.

Integer-only Parameter Values

Let’s take a look at this step from the example feature:

Then the response contains exactly 1 location

You can imagine that it might be useful to be able to reuse this step in other scenarios to verify that retrieving location data for a given combination of country code and zip code results in a different number of locations. However, it doesn’t make sense to accept all possible parameter values, only integer values.

You can do this with regular expressions as follows:

[Then(@"the response contains exactly (\d+) location")]
public void ThenTheResponseContainsExactlyLocation(int expectedNumberOfPlacesReturned)
{
}

The (\d+) in the regular expression restricts the matching values to integers. Also, the + sign makes sure that the parameter value isn’t empty, as it only accepts parameter values that consist of at least one digit. This means that the regular expression is now matched by both

Then the response contains exactly 1 location

as well as

Then the response contains exactly 99 location

but not by

Then the response contains exactly banana location

Also, we can now safely set the data type of the step definition method parameter to int, because our regular expressions ensure that only integer values are being passed on.

Optional Characters

There’s a negative side effect, however, to making the step more flexible as in the example above. When more than one location is returned by the API, you’ll have to use locations in the step to define your expectations in a grammatically correct manner, not location.

Copying and pasting the step definition with only a slightly different regular expression (only an extra ‘s’) with (presumably) the exact same method body doesn’t make sense either. Instead, you can use the following solution, again made possible by the power of regular expressions:

[Then(@"the response contains exactly (\d+) locations?")]
public void ThenTheResponseContainsExactlyLocation(int expectedNumberOfPlacesReturned)
{
}

The question mark ? behind the s character implies that the latter is an optional character, so both steps containing location as well as locations are matched by the regular expression now.

Restricting Parameter Values to a List of Options

As a final example of using the power of regular expressions in your SpecFlow features, consider this step from the example feature:

Then the response has status code 200

Let’s now assume that the API is built to only return HTTP status code 200 (when a country code and zip code combination is supplied that exists in the service database), HTTP status code 404 (when the combination is not present in the database) or HTTP 500 (when an internal server error occurs).

In this case, using (\d+) to accept only integer values in your steps may not be restrictive enough, because you only want to allow specific integer values. Regular expressions come to the rescue here, too:

[Then(@"the response has status code (200|404|500)")]
public void ThenTheResponseHasStatusCode(int expectedStatusCode)
{
}

Now, only the aforementioned integer values 200, 404 and 500 are matched by the regular expression, while other integer values are not.

There are many more things you can do with regular expressions in SpecFlow, but I find myself using the above three examples regularly.

In the next article, we’ll be taking a closer look at some of the options provided by SpecFlow to further clean up your feature files and scenarios and avoid repeating steps, in an effort to make your features even more readable and powerful.

The example project used in this article can be found on GitHub: https://github.com/basdijkstra/testproject-specflow.

 

About the author

Bas Dijkstra

Bas teaches companies around the world how to improve their testing efforts through test automation. He is an independent trainer, consultant and developer living in the Netherlands. When he’s not working he likes to take his bicycle for a ride, go for a run or read a good book.

Leave a Reply

FacebookLinkedInTwitterEmail