This is a followup post to my cucumber style guide. I felt it was getting too long, so I split this topic off into its own post.
The Problem
Sooner or later you will have steps in your feature file that are the same in many places except for a word or phrase. Usually this is something like a user name, screen element, action, or index. For example, if you had the following scenarios (or more than this of the same form):
Scenario: One
Given The user is on the home screen
Then The user sees the search bar element
And sees the categories bar element
...
Scenario: Two
Given The user is on the settings screen
Then The user sees the username element
And sees the password element
...
Each line would need its own step definition, but what those steps do would basically be the same. In this case the Given
statements would verify we are on the proper screen and the others verify some UI element is present.
It would be better if there was a way to eliminate duplicate code in the step definitions by collapsing these similar steps. Fortunately, this is possible and there is a way to expand upon that to be even more flexible.
The Simple Solution
The simple way to collapse the step code is to make use of step arguments. Step arguments allow dynamic values to be extracted during the step matching process. See here for details about how to do this via regular or Cucumber expressions.
Refactoring the above example involves making the screen name a parameter for the "is on" steps and the element name a parameter for the "sees the" steps. While you can change the step matchers only, it is a good idea to add some delimiters around the dynamic values. This marks them clearly in the feature file as well as making it easier to write the matchers.
The feature file now looks like this:
Scenario: One
Given The user is on the "home" screen
Then The user sees the "search bar" element
And sees the "categories dropdown" element
...
Scenario: Two
Given The user is on the "settings" screen
Then The user sees the "username" element
And sees the "password" element
...
The step code changes to this (note, it is written in Typescript and uses regular expressions for matching):
Then(/^The user is on the "(.*)" screen$/, async (screen: string) => {
switch (screen.toLowerCase()) {
case 'home':
await homeScreen.isScreenPresent()
break
case 'settings':
await settingsScreen.isScreenPresent()
break
default:
throw new Error('Invalid screen name')
}
})
Then(/^The user sees the "(.*)" element$/, async (element: string) => {
switch (element.toLowerCase()) {
case 'search bar':
case 'categories dropdown':
await homeScreen.isElementVisible(element)
break
case 'username':
case 'password':
await settingsScreen.isElementVisible(element)
break
default:
throw new Error('Invalid element name')
}
})
You can see how we capture everything within the quotes, which gets passed as the single value to the associated method. A switch statement executes code specific to this value. As seen in the second step, case
statements can be grouped together if the call to supporting code is the same.
While this is an improvement over single steps for everything, it is very likely the code called in the case
statements will still be very similar to each other. If we are clever, we may be able to further collapse the step code.
There is also the problem with name collisions in the switch
statements. However, these can be fixed with rules around how we name values passed to the step definitions. In our example, if two screens had an email field, we would need to name them "screen 1 email" and "screen 2 email". The screen name must be passed to the step in order to have two unique case
statements.
A Better Solution
The cleverness mentioned above involves the use of factories and interfaces. A factory is used to instantiate the object to act upon instead of using shared global instances. Since these objects will all be similar, they can implement an interface of common actions. Together this allows the lengthy switch
statements to be replaced with a single line:
Then(/^The user is on the "(.*)" screen$/, async (screen: string) => {
await ScreenFactory.getScreen(screen).isScreenPresent()
})
Then(/^The user sees the "(.*) > (.*)" element$/, async (screen: string, element: string) => {
await ScreenFactory.getScreen(screen).isItemVisible(element)
})
For our example the factory returns a screen instance. We need to pass the name of the screen to get, which is extracted directly from the cucumber text.
Note, to get the screen name when specifying a element we introduced the naming rule of "Screen > Element". The use of ">" makes the extraction regular expression much easier to code.
It also helps make the feature file text better by clearly dividing sections of the name. This is especially beneficial with deeper elements (Ex: Settings > Integrations > Email > Server).
Once we have our object to act on, we can call the actual action with the appropriate parameters. Since the factory generated objects all implement the same interface, we guarantee that these actions are valid all of the time. If we do happen to have any exceptions, we can still have some logic to account for those as needed.
Conclusion
No matter what you are automating using Cucumber, patterns in the text will form. By imposing simple rules about naming the dynamic shared portions, you can simplify the underlying step definition code. Also, by using the factory pattern along with interfaces the step definition code become cleaner and even more compact.
Obviously I am highly biased, but I like this pattern since it results in simple, readable step definition code as well as very clear feature file text. I hope you will give it a try.
Top comments (0)