Targeting Multiple Browser with a Single Test with SpecFlow+ 3

(Note: This is an updated version of this article for SpecFlow+ 3. If you are looking for the article for earlier versions of SpecFlow, it can be found here.)

If you are testing a web app (e.g. with Selenium), you will normally want to test it in a range of browsers, e.g. Chrome, IE/Edge and Firefox. However, writing tests for all the browsers can be a time-consuming process. Wouldn’t it be much easier to just write just one test, and be able to run that test in all browsers?

That’s where using targets with the SpecFlow+ Runner comes in. Targets are defined in your SpecFlow+ Runner profile. They allow you to define different environment settings, filters and deployment transformation steps for each target. Another common use case is to define separate targets for X64 and x86.

Defining targets for each browser allows us to execute the same test in all browsers. You can see this in action in the Selenium sample project available on GitHub. If you download the solution and open Default.srprofile, you will see 3 different targets defined at the end of the file:

<Targets>
  <Target name="IE">
    <Filter>Browser_IE</Filter>
    <DeploymentTransformationSteps>
      <EnvironmentVariable variable="Test_Browser" value="IE" />
    </DeploymentTransformationSteps>
  </Target>
  <Target name="Chrome">
    <Filter>Browser_Chrome</Filter>
    <DeploymentTransformationSteps>
      <EnvironmentVariable variable="Test_Browser" value="Chrome" />
    </DeploymentTransformationSteps>
  </Target>
  <Target name="Firefox">
    <Filter>Browser_Firefox</Filter>
    <DeploymentTransformationSteps>
      <EnvironmentVariable variable="Test_Browser" value="Firefox" />
    </DeploymentTransformationSteps>
  </Target>
</Targets>

Each of the targets has a name and an associated filter (e.g. “Browser_IE”). The filter ensures that only tests with the corresponding tag are executed for that target.

For each target, we transform the Test_browser environment variable to contain the name of the target. This will allow us to know the current target and access the corresponding web driver for each browser. WebDriver.cs (located in the Drivers folder of the TestApplication.UiTests project) uses this key to instantiate a web driver of the appropriate type (e.g. InternetExplorerDriver). Based on the value of this environment variable, the appropriate web driver is returned by GetWebDriver() is passed to BrowserConfig, used in the switch statement:

private IWebDriver GetWebDriver()
{
  switch (Environment.GetEnvironmentVariable("Test_Browser"))
  {
    case "IE": return new InternetExplorerDriver(new InternetExplorerOptions { IgnoreZoomLevel = true }) { Url = SeleniumBaseUrl };
    case "Chrome": return new ChromeDriver { Url = SeleniumBaseUrl };
    case "Firefox": return new FirefoxDriver { Url = SeleniumBaseUrl };
    case string browser: throw new NotSupportedException($"{browser} is not a supported browser");
    default: throw new NotSupportedException("not supported browser: <null>");
  }
}

Depending on the target, the driver is instantiated as either the InternetExplorerDriver, ChromeDriver or FirefoxDriver driver type. The bindings code simply uses the required web driver for the target to execute the test; there is no need to write separate tests for each browser. You can see this at work in the Browser.cs and CalculatorFeatureSteps.cs files:

[Binding]
public class CalculatorFeatureSteps
{
  private readonly WebDriver _webDriver;

  public CalculatorFeatureSteps(WebDriver webDriver)
  {
    _webDriver = webDriver;
  }
       
  [Given(@"I have entered (.*) into (.*) calculator")]
  public void GivenIHaveEnteredIntoTheCalculator(int p0, string id)
  {
    _webDriver.Wait.Until(d => d.FindElement(By.Id(id))).SendKeys(p0.ToString());
  }

To ensure that the tests are executed, you still need to ensure that the tests have the appropriate tags (@Browser_Chrome, @Browser_IE, @Browser_Firefox). 2 scenarios have been defined in CalculatorFeature.feature:

@Browser_Chrome
@Browser_IE
@Browser_Firefox
Scenario: Basepage is Calculator
	Given I navigated to /
	Then browser title is Calculator

@Browser_IE 
@Browser_Chrome
Scenario Outline: Add Two Numbers
	Given I navigated to /
	And I have entered <SummandOne> into summandOne calculator
	And I have entered <SummandTwo> into summandTwo calculator
	When I press add
	Then the result should be <Result> on the screen

Scenarios: 
		| SummandOne | SummandTwo | Result |       
		| 50         | 70         | 120    | 
		| 1          | 10         | 11     |

Using targets in this way can significantly reduce the number of tests you need to write and maintain. You can reuse the same test and bindings for multiple browsers. Once you’ve set up your targets and web driver, all you need to do is tag your scenarios correctly. If you select “Traits” under Group By Project in the Test Explorer, the tests are split up by browser tag. You can easily run a test in a particular browser and identify which browser the tests failed in. The test report generated by SpecFlow+ Runner also splits up the test results by target/browser.

Remember that targets can be used for a lot more than executing the same test in multiple browsers with Selenium. Don’t forget to read the documentation on targets, as well as the sections on filters, target environments and deployment transformations.

Generating Code Behind Files using MSBuild

Since SpecFlow 1.9, your can generate the code-behind files for feature files (*.feature.cs) at compile time. To do so, you need to use a MSBuild Task. The documentation is here.

Pros

  • Feature files and code-behind files are always in sync
  • No need to check the feature.cs files into your source control system
  • Works without Visual Studio
  • Works for both .NET Full Framework and .NET Core

Cons

  • When adding a new file, the CustomTool entered in the feature file’s properties currently has to be removed each time
  • Realtime test discovery will only find new tests after the project has been (re)built

Best practises

Store code-behind files in same folder as the feature file

In the past, we recommended moving the generated code-behind files to a different folder from your feature files.
We no longer recommend this approach, as you will otherwise experience problems with up-to-date checks in MSBuild.

Additionally, Microsoft has since fixed a bug in VS, meaning that navigating from the Test Explorer to the feature file works again (see here). For this to work, the code-behind files need to be located by VS, and having the generated files in a separate folder will break this feature again.

Known Bugs

  • Prior to SpecFlow 2.4.1, Visual Studio sometimes does not recognize that a feature file has changed. To generate the code-behind file, you therefore need to rebuild your project. We recommend using SpecFlow 2.4.1 or higher, where this is no longer an issue.

Enabling MSBuild Code Behind Generation

Classic Project System

  1. Add the NuGet package SpecFlow.Tools.MsBuild.Generation with the same version as SpecFlow to your project.
  2. Remove all SpecFlowSingleFileGenerator custom tool entries from your feature files.

SDK style project system

Please use at least SpecFlow 2.4.1, as this version fixes the above issue in 2.3.*.

  1. Add the NuGet package SpecFlow.Tools.MsBuild.Generation with the same version as SpecFlow to your project
  2. Remove all SpecFlowSingleFileGenerator custom tool entries from your feature files.

Common issues

After upgrading the NuGet packages, the code-behind files are not generated at compile time

If you are using the classic project system, the previous MSBuild target may no longer located at the end of your project. NuGet ignores entries added manually, and places the MSBuild imports at the end.  However, the AfterUpdateFeatureFilesInProject target needs to be defined after the imports, because otherwise it will be overwritten with an empty definition. If this happens, your code-behind files are not compiled as part of the assembly.

Linked files are not included

If you link feature files into a project, no code-behind file is generated for them (see GitHub Issue 1295).

More infos about MSBuild:

Improving Gherkin Specs with SpecFlow Step Argument Transformations

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:

"3" (string) becomes 3 (integer) - this is the standard conversion

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 StepArgumentTransformationn 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:

"no" is evaluated by the method and returns the integer value 0, used by itemsInStock

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 by 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 CreateSet<T> or CreateInstance<T> methods). They can be used with paremeters 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.

Discover BDD with Gaspar Nagy and Seb Rose

Gaspar Nagy – who has been involved with SpecFlow since its inception – and Seb Rose – a core member of the open source Cucumber project – have teamed up to publish Discovery – Explore behaviour using examples. This is the first book in a series on BDD. Gaspar had the following to say about the motivation behind writing the book, and the type of content you can expect.

 

When it comes to BDD, there are generally 3 types of people:

  1. Those who do not know what BDD stands for at all (how sad!)
  2. Those who know BDD, and claim to implement it with their team, but where it turns out that their focus is only on the tools
  3. Those for whom BDD is a holistic approach

The initial idea was for a SpecFlow book that would essentially be a port of the “Cucumber for Java” book: a collection of technical tips etc. In other words, yet another technical book. While working on the core, it quickly became apparent that a SpecFlow book would never be complete without mentioning the collaborative aspects of BDD. So we ended up diving deeper into the BDD realm. At some point we made a decision: let this book be about BDD, instead of SpecFlow. In fact, let there be a BDD book series!

Why? Because in our opinion the BDD process can be divided into 3 stages:

  1. Discovery: explore and discover details through examples
  2. Formulation: convert examples to scenarios
  3. Automation: write test automation code

My painful experience is that the earlier a team adopts BDD, the better results they get. So why not give teams a helping hand (or even a compass) right at the start, in the discovery phase? We decided that this would be the essence of the first book in the series.

It took several months of intensive work with Seb to complete the 5 chapters of Discovery. At about 100 pages, it is relatively short, and said to be an easy read, so you could read it on a plane. The book is written for everyone: developers and testers, as well as product owners and test managers. Discovery – Explore behaviour using examples is also tool-agnostic: the emphasis is not on the technical process but on the collaborative aspect. The book covers the following:

Chapter 1 provides an introduction to the nature of BDD, where the important elements are explained.

Chapter 2 is an important chapter about structured conversation, providing hints on how testers and business stakeholders can be better involved in meetings, and how a shared understanding of the requirements can be achieved. A number of techniques, in particular Example Mapping (introduced by Matt Wynne), are described here through imaginative use cases.

Chapter 3 is about examples, and how there are more than just Given/When/Then test cases.

Chapter 4 covers tips on how a BDD approach can fit your project, regardless of whether you use Scrum, Kanban or any other framework.

Chapter 5 is all about how to sell BDD to business stakeholders, and make them interested in BDD. It includes tips on how to demonstrate that BDD is more than a mere technique applicable only to the tasks of developers and testers. Illustrative examples ensure that business stakeholders can relate to the advantages and share a common understanding as to why BDD is good for the entire team, saving time and effort on bug hunting. In order to reap the biggest benefits of BDD, the business side should be involved in the entire process from the start to finish.

Get your copy and let the discovery begin! You can find it on Amazon/Leanpub through http://bddbooks.com!

 

Targeting Multiple Browsers with a Single Test

(Note: This article was written for SpecFlow+ Runner 1.8 and SpecFlow 2; an updated version of this article for SpecFlow+ 3 is here.)

If you are testing a web app (e.g. with Selenium), you will normally want to test it in a range of browsers, e.g. Chrome, IE/Edge and Firefox. However, writing tests for all the browsers can be a time-consuming process. Wouldn’t it be much easier to just write just one test, and be able to run that test in all browsers?

That’s where using targets with the SpecFlow+ Runner comes in. Targets are defined in your SpecFlow+ Runner profile. They allow you to define different environment settings, filters and deployment transformation steps for each target. Another common use case is to define separate targets for X64 and x86.

Defining targets for each browser allows us to execute the same test in all browsers. You can see this in action in the Selenium sample project available on GitHub. If you download the solution and open Default.srprofile, you will see 3 different targets defined at the end of the file:

<Targets>
  <Target name="IE">
    <Filter>Browser_IE</Filter>
  </Target>
  <Target name="Chrome">
    <Filter>Browser_Chrome</Filter>      
  </Target>
  <Target name="Firefox">
    <Filter>Browser_Firefox</Filter>
  </Target>
</Targets>

Each of the targets has a name and an associated filter (e.g. “Browser_IE”). The filter ensures that only tests with the corresponding tag are executed for that target.

For each target, we are going to transform the browser key in the appSettings section of our app.config file to contain the name of the target. This will allow us to know the current target and access the corresponding web driver for each browser. The corresponding section in the project’s app.config file is as follows:

  <appSettings>
    <add key="seleniumBaseUrl" value="http://localhost:58909" />
    <add key="browser" value="" />
</appSettings>

To make sure that the name of the target/browser is entered, we transform the app.config file to set the browser key’s value attribute to the name of the target using the {Target} placeholder. This placeholder is replaced by the name of the current target during the transformation:

<Transformation>
  <![CDATA[<?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
      <appSettings>
        <add key="browser" value="{Target}"
        xdt:Locator="Match(key)"
        xdt:Transform="SetAttributes(value)" />
      </appSettings>
    </configuration>
  ]]>
</Transformation>

This section locates the key (browser) and sets its value attribute to the name of the target. This transformation occurs separately for all three targets, resulting in 3 app.config files, each with a different browser key (that is available to your application).

WebDriver.cs (located in the Drivers folder of the TestApplication.UiTests project) uses this key to instantiate a web driver of the appropriate type (e.g. InternetExplorerDriver). The key from the configuration file is passed to BrowserConfig, used in the switch statement:

get
{
  if (_currentWebDriver != null)
    return _currentWebDriver;

  switch (BrowserConfig)
  {
    case "IE":
      _currentWebDriver = new InternetExplorerDriver(new InternetExplorerOptions() { IgnoreZoomLevel = true }) { Url = SeleniumBaseUrl };
      break;
    case "Chrome":
      _currentWebDriver = new ChromeDriver() { Url = SeleniumBaseUrl };
      break;
    case "Firefox":
      _currentWebDriver = new FirefoxDriver() { Url = SeleniumBaseUrl };
      break;
    default:
      throw new NotSupportedException($"{BrowserConfig} is not a supported browser");
    }

  return _currentWebDriver;
}

Depending on the target, the driver is instantiated as either the InternetExplorerDriver, ChromeDriver or FirefoxDriver driver type. The bindings code simply uses the required web driver for the target to execute the test; there is no need to write separate tests for each browser. You can see this at work in the Browser.cs and CalculatorFeatureSteps.cs files:

[Binding]
public class CalculatorFeatureSteps
{
  private readonly WebDriver _webDriver;

  public CalculatorFeatureSteps(WebDriver webDriver)
  {
    _webDriver = webDriver;
  }
       
  [Given(@"I have entered (.*) into (.*) calculator")]
  public void GivenIHaveEnteredIntoTheCalculator(int p0, string id)
  {
    _webDriver.Wait.Until(d => d.FindElement(By.Id(id))).SendKeys(p0.ToString());
  }

To ensure that the tests are executed, you still need to ensure that the tests have the appropriate tags (@Browser_Chrome, @Browser_IE, @Browser_Firefox). 2 scenarios have been defined in CalculatorFeature.feature:

@Browser_Chrome
@Browser_IE
@Browser_Firefox
Scenario: Basepage is Calculator
	Given I navigated to /
	Then browser title is Calculator

@Browser_IE 
@Browser_Chrome
Scenario Outline: Add Two Numbers
	Given I navigated to /
	And I have entered <SummandOne> into summandOne calculator
	And I have entered <SummandTwo> into summandTwo calculator
	When I press add
	Then the result should be <Result> on the screen

Scenarios: 
		| SummandOne | SummandTwo | Result |       
		| 50         | 70         | 120    | 
		| 1          | 10         | 11     |

Using targets in this way can significantly reduce the number of tests you need to write and maintain. You can reuse the same test and bindings for multiple browsers. Once you’ve set up your targets and web driver, all you need to do is tag your scenarios correctly. If you select “Traits” under Group By Project in the Test Explorer, the tests are split up by browser tag. You can easily run a test in a particular browser and identify which browser the tests failed in. The test report generated by SpecFlow+ Runner also splits up the test results by target/browser.

Remember that targets can be used for a lot more than executing the same test in multiple browsers with Selenium. Don’t forget to read the documentation on targets, as well as the sections on filters, target environments and deployment transformations.

Fit-for-purpose Gherkin

A common mistake I see is the tendency to write Gherkin specification as if they were a series of instructions describing the algorithm for how the test is executed: “enter X” or “open page Y”. This is understandable; after all, it’s how you might write a standard test script. It’s very easy to continue using the same approach with Gherkin.

But the power of Gherkin comes from its abstraction – the ability to formulate steps in plain English (or any of the other supported languages), rather than as an algorithm. Instead of describing how something is done, Gherkin should describe what is being done without worrying about the implementation details.

This is a powerful tool, separating the algorithmic steps (bindings) from the actual description of the test intended to be read by humans. The primary goal of Gherkin is to formulate your scenarios so that they are quick to read, clearly formulated and understandable to all stakeholders – including non-technical team members.

OK, but what does that mean in real practical terms? Let’s take a look at a very basic example where we have a web application with various user roles, including administrators. Any test interfacing with the UI that involves the administrator dashboard will require the following initial steps in order to execute the test:

  1. Open the login page by navigating to the appropriate URL.
  2. Enter the user name.
  3. Enter the password.
  4. Click on the Log In button
  5. Select Administrator Dashboard from the menu.

These steps are important– you will definitely need to log in to the system somehow and navigate to the appropriate area to perform to execute the test. But let’s take a step back from the nitty gritty details, and ask ourselves what our precondition is: we need to be logged in as administrator, and we need to be viewing the administrator dashboard.

So, as tempting as it is to translate each of the steps listed above into individual steps in Gherkin, it is much more advisable to write the following:

Given I am logged in with administrator privileges

And I am viewing the administrator dashboard

These statement clearly capture the current state of the system required in order to perform the test. It goes without saying that being logged in as administrator involves logging in, but how this is done is not where the focus of your discussion should be. These steps are implicit implementation details that add nothing to the discussion about what can be done on the administrator dashboard itself. Furthermore, you may interface directly with your API and not even necessarily perform these steps using the UI.

The only place you need to worry about the implementation itself – how you log in to the system – is in the binding code for the Given step. There is no reason to explicitly state these steps in Gherkin; in fact, doing so runs the risk of making the feature file bloated, and thus harder to understand and navigate. As a side effect, you will also end up with a lot of unnecessary bindings for the unnecessary steps that could easily be grouped together.

Now if you have been critically evaluating the previous Gherkin step, you might even argue that “Given I am viewing the administrator dashboard” already implies you are logged in as administrator. And to a certain extent I would agree with you. In this case, my preferred approach would be to move the first step (Given I am logged in with administrator privileges) to the scenario background – assuming the feature file I am working on covers a range of administrator functions, all of which require the user to be logged in as an admin. Using background statements is another great way to reduce the clutter in your Gherkin files.

Another thing to remember is that there are always implicit steps; the question is where to draw the boundary. It is probably unnecessary to specify “Given I am logged in” as the prerequisite for every test, just like it is unnecessary to add the step “Given I have started the application”. Of course, you will want to specify the login state for certain, specific tests: testing the login process presumably requires the user to not already be logged in, so this is a step that should be specified. Similarly, access to the administrator dashboard is limited to a certain user type – being logged in as an administrator is therefore an important criteria. In this case, it also includes the information that other user types cannot perform these actions.

So let’s come back to the actual implementation of the steps. We have reduced a number of explicit steps to a single Gherkin statement, but we still need to execute all of those steps. That’s easily handled by writing code for each of the necessary steps that is called from the binding:

public void GivenIAmLoggedInWithAdminPrivileges()
{
    NavigateTo(URL);
    EnterUserName(“Admin”);
    EnterPassword(“P@ssw0rd”);
    ClickLogInButton(); 
}

As mentioned above, this might be abstracted to simply interface with your API, rather than actually performing these steps via the UI. Of course, just like you can reduce multiple explicit steps to a single Gherkin step, you can also group different sub-functions within a parent function, which I would probably do here:

public void GivenIAmLoggedInWithAdminPrivileges()
{
     Login(“Admin”,“P@ssw0rd”);
}

So I have one single function to handle the login process, which can obviously be reused elsewhere. Furthermore, any changes to the login process only require a single change to the test automation code and do not require any changes to the Gherkin file.

This makes it easier to maintain your tests and reuse steps that represent logical units, rather than having multiple tests share a large number of mini-steps that are brittle. Changes to the login process or your API do not require the test to be rewritten; they only affect the bindings of the tests. Furthermore, the step Given I am logged in as with administrator privileges can be used by any test involving an administrator account in a single binding method.

Of course, these concepts are not new. You will at some point have taken a function that has grown over time, and split it up into smaller sub-functions. You should adopt a similar approach to Gherkin: it may help to try and think more in terms of general functions, rather than individual statements. This may require a paradigm shift on your part: instead of being as explicit as possible in your feature files, reduce the steps to the essentials without obscuring any key information. The less bloated and more concise your tests/specifications are, the better suited they are to encouraging meaningful discussion between stakeholders, and the more understandable they are to non-technical team members. At the end of the day, that is probably the biggest benefit to this approach – it enables everyone to understand what is (supposed to be) going on and why.

As with any code (yes, feature files are code files too), a certain amount of refactoring and maintenance will always be required as your project grows. However, ensuring that your Gherkin files are fit-for-purpose from the start will mean that this refactoring will be a lot easier and less frequent. It will also have far less of an impact on your binding code if you reduce the frequency with which you need to update your Gherkin steps.