The next Given-When-Then with style challenge is to remove duplication from similar scenarios, in particular when groups of steps are shared between different scenarios.
How can we call one scenario from another? We have several large feature files with a lot of duplication, and some smaller scenarios that contain blocks of steps from the large ones. We’d like to use smaller scenarios as components for the big ones.
A typical example of this problem is describing workflows, when different workflow branches may require same or very similar paths to test. Here is a sample involving user registration functions:
Feature:
Scenario: User registers
Given a visitor opens the homepage
And the visitor enters a new username into the registration form
And the visitor enters a valid password into the registration form
When the visitor submits the registration form
Then the registration should be successful
And a new user account should be created
And the visitor is be redirected to the login form
Scenario: User signs in after registration
Given a visitor opens the homepage
And the visitor enters a new username into the registration form
And the visitor enters a valid password into the registration form
When the visitor submits the registration form
And the visitor is redirected to the login form
And the visitor enters the username and password into the sign in form
When the visitor submits the sign in form
Then the signed in user page should be shown
And the visitor username should be displayed as the account name
Scenario: User requests password reset
Given a visitor opens the homepage
And the visitor enters a new username into the registration form
And the visitor enters a valid password into the registration form
When the visitor submits the registration form
And the visitor is redirected to the login form
And the visitor clicks the forgot password link in the sign in form
Then the visitor is redirected to the forgot password form
When the visitor enters his registered email
And the visitor submits the forgot password form
Then the password reset notification should be sent
And the visitor should be redirected to the password reset completion form
Scenario: User completes password reset
...
A trivial solution for this case would be to create a common background. But to further complicate the challenge, let’s add to this cases where the shared steps are not just at the top of each scenario, but could be in the middle or at the end as well.
Feature: new account purchases
Scenario: User tops-up balance immediately after registration
Given a visitor opens the homepage
And the visitor enters a new username into the registration form
And the visitor enters a valid password into the registration form
When the visitor submits the registration form
And the visitor is redirected to the login form
And the visitor enters the username and password into the sign in form
When the visitor submits the sign in form
And the signed in user page is shown
And the user opens the balance page
And the user adds credit card information on the payment form
And the user selects 10 EUR as the top-up amount
And the user submits the payment form
And the payment completes
Then the user balance should show 10 EUR
And the user clicks the log-out button
Then the user is logged out
Scenario: User tops-up balance after re-login after registration
Given a visitor opens the homepage
And the visitor enters a new username into the registration form
And the visitor enters a valid password into the registration form
When the visitor submits the registration form
And the visitor is redirected to the login form
And the visitor enters the username and password into the sign in form
When the visitor submits the sign in form
And the signed in user page is shown
And the user clicks the log-out button
Then the user is logged out
And the visitor enters the username and password into the sign in form
When the visitor submits the sign in form
And the signed in user page is shown
And the user opens the balance page
And the user adds credit card information on the payment form
And the user selects 10 EUR as the top-up amount
And the user submits the payment form
And the payment completes
Then the user balance should show 10 EUR
And the user clicks the log-out button
Then the user should be logged out
As the larger scripts get more complicated, they become more difficult to manage, and the duplication starts to hurt a lot. Reusing parts of scenarios (or entire scenarios) is a common feature of test management tools, and people coming to Given-When-Then specs from a testing background often look for a way to call one scenario from another, or reuse flows.
How would you rewrite these scenarios to avoid duplication?
The challenge was to remove duplication from similar scenarios, in particular when they share groups of steps. See the original post for a detailed description of the problem. This article contains two ways of solving the problem. The first approach almost implements the proposed solution, calling the steps of one scenario from another. The second approach provides a much better way of solving the same problem.
Once a team builds up a library of useful Given-When-Then steps, testers or analysts can often write new scenarios that require little or no developer involvement to automate. Scenarios with shared steps usually call the same step bindings. A step that’s worded the same, or very similarly, in two different scenarios will typically have a single implementation, as in the picture below:
This is great for individual steps, but once scenarios start to use similar blocks of steps, the duplication starts to hurt. People who maintain the Given-When-Then feature files then usually look for some way of sharing entire blocks of steps, for example one scenario executing another.
This kind of sharing is not directly possible with SpecFlow. To the best of my knowledge, no other Given-When-Then tools support this kind of sharing.
This might sounds like a missing key feature, but it’s not. In fact, it would be a very bad practice to compose scenarios that way. However, let’s first look at how to implement something close to the proposed solution.
From an analysis perspective, scenarios should be focused on what’s being tested and not how a test is executed. Very often, when there are entire blocks of steps shared between scenarios, the steps are describing the mechanics of testing instead of the purpose of a test. There’s usually a higher level concept in play. In the original problem post, almost all scenarios share a block of steps that ensures a user is signed in:
Given a visitor opens the homepage
And the visitor enters a new username into the registration form
And the visitor enters a valid password into the registration form
When the visitor submits the registration form
And the visitor is redirected to the login form
And the visitor enters the username and password into the sign in form
When the visitor submits the sign in form
Instead of calling one scenario from another, it would be better to create a higher level concept and use it everywhere instead of that block. For example, let’s just create a step that sets up a session for a logged in user:
Given a user is logged in
We can then implement this step by reusing the automation components in a lower level.
There are several ways of implementing higher-level steps. One common, but mistaken approach, would be to just call lower-level steps from higher-level steps. SpecFlow does support such calls, allowing us to to implement the sharing just below the feature file layer, in the step implementations.
This kind of abstraction is easy to implement initially. Instead of making the implementation for a higher level step connect to the system under test, it can invoke other steps. The implementation could in theory look as in the snippet below:
[Given(@"a user is logged in")]
public void GivenUserIsLoggedIn()
{
Given("a visitor opens the homepage");
Given("the visitor enters a new username into the registration form");
Given("the visitor enters a valid password into the registration form");
Given("the visitor submits the registration form");
Given("the visitor is redirected to the login form");
Given("the visitor enters the username and password into the sign in form");
}
The problem with this approach is that we’re simulating sub-procedures, and effectively programming in a language that’s not designed for code. In simple cases this might be OK, but for complex situations this quickly leads to an unmaintainable mess. The approach is error-prone, and problems are difficult to spot and fix.
For more information on this feature, check out the Calling Steps from Step Definitions page in SpecFlow documentation. However, I would not recommend using this approach. Neither would the people who maintain SpecFlow. This feature is deprecated, and will be removed from one of the upcoming SpecFlow releases, exactly because it leads to a bad unmaintainable mess.
Avoid programming in Given/When/Then. Use a programming language for that. You’ll get good compilation support, type checking, and all other benefits of modern programming languages.
One of the best things about SpecFlow and similar tools is that they separate the system under test from the textual scenarios using a very thin layer of code. This layer should be thin, but it doesn’t have to be just one level deep.
The right way to share implementation between different steps would be to introduce another level of code. Instead of calling steps through SpecFlow, create a utility class that captures user interactions with the login form, and then directly manipulate it from various step implementations.
Utilities such as interface drivers and page objects belong to this intermediate layer between step implementations and the system under test. Don’t expose user interactions directly to Gherkin steps, as that leads to using an automation language in specifications, instead of focusing on clearly describing the problem domain. Encapsulate them into something meaningful for your product, and simplify the code in the steps.
These utility classes can become relatively complex for large systems, and that’s perfectly fine. It’s better to manage test workflow complexity in code than in plain text. You can keep removing duplication and extracting layers. In case of user-interface tests, I usually like to split out at least three levels: business rules, workflows and technical activities. Different business rules might reuse parts of different workflows (such as the sign-in process or purchasing), and various workflows might use the same technical components in different ways (for example the shopping cart or the login form). For more information on this approach, check out my article How to implement UI testing without shooting yourself in the foot.
As a side note, sometimes these utility classes become useful for other types of tests, for example for technical integration tests, or as quick drivers for component tests. This significantly increases the value of that middle testing layer, and provides an even higher incentive to abstract things away from SpecFlow step implementations.
Finally, I’d like to leave you with a guideline to know when to reuse steps, and when not.
There are two typical situations with reusing steps, and that it’s important to spot which one you’re dealing with.
When the step library reflects the business domain, sharing steps across scenarios promotes using a ubiquitous domain language, and helps teams achieve better understanding of the problem they are trying to solve.
On the other hand, when the shared steps work as low-level automation components, the same approach leads away from ubiquitous domain language, and causes a long-term maintenance headache.
Whenever you get the urge to share steps across scenarios, consider whether you’re doing it because the steps really fit the problem space, or if they’re just low-level automation components. If it’s the latter case, it’s always better to create an intermediate automation utility layer and push the reuse there.
The next Given-When-Then with style challenge is about creating good scenario titles.