DEV Community

Vitaliy Potapov
Vitaliy Potapov

Posted on

Solving a problem of duplicate steps in Cucumber BDD testing

Hello πŸ‘‹

I'm a maintainer of BDD testing tool. One of the popular requests I'm getting from consumers - to allow duplicate step definitions bound to different features. For example, I need to test an application that has "game" and "video-player" pages. Both pages have PLAY button in the interface. I write two scenarios:

game.feature

Given I have not started a game yet
When I click the PLAY button # <- duplicated step
Then the game begins
Enter fullscreen mode Exit fullscreen mode

video-player.feature

Given I am watching a youtube video
When I click the PLAY button # <- duplicated step
Then the video plays
Enter fullscreen mode Exit fullscreen mode

Step implementation for I click the PLAY button is different for each feature.

Official Cucumber docs says it's an anti-pattern and step definitions should be globally unique. Proposed workaround is to modify step pattern to avoid the ambiguity. E.g.:

When('I click the PLAY button in game', ...);
When('I click the PLAY button in video player', ...);
Enter fullscreen mode Exit fullscreen mode

That's annoying!

Existing solutions

Non of official Cucumber implementations supports duplicate steps. They report ambiguous step error once your step matches more than one step definition.

Cucumber plugin for Cypress introduced interesting feature called Paring. It allows to have duplicate step definitions paired to particular features via special configuration pattern. For example, having a files structure:

└── features/
    β”œβ”€β”€ steps/
    β”‚   β”œβ”€β”€ common.ts
    β”‚   β”œβ”€β”€ game.ts
    β”‚   └── video-player.ts
    β”œβ”€β”€ game.feature
    └── video-player.feature  
Enter fullscreen mode Exit fullscreen mode

I can configure step paths with special keyword [filepath]:

stepDefinitions: [
    'features/steps/common.ts',
    'features/steps/[filepath].ts', // <- pair steps to particular feature
]
Enter fullscreen mode Exit fullscreen mode

During steps loading, [filepath] will be replaced with actual feature name and these steps will be paired to the feature. Now it is possible to have separate step definitions I click the PLAY button for "page" and "video-player".

Drawbacks

Although I like that pairing technique, I see two drawbacks:

  1. You can't just define steps as a single string pattern, see a common mistake. You should make it more complex, splitting on common steps + pairing pattern steps.

  2. Pairing can't be resolved without reading the configuration. That is mostly for tools like IDE extensions, for navigating to step definition by cmd + click. Currently, the most popular one does not support it, but hopefully will.

Proposed solution

While thinking about steps pairing in Cypress plugin, I've got another idea how it can be implemented. The solution is inspired by Next.js route groups.

We can introduce steps scope - a file or directory with name in parenthesis, e.g. (game) or (video-player).

Step definitions inside scoped directory are applicable only to features inside that directory.

This is the only rule one should know to understand the approach.

Now we can define the file structure:

└── features/
    β”œβ”€β”€ steps/
    β”‚   └── common.ts
    β”œβ”€β”€ (game)/
    β”‚   β”œβ”€β”€ game.feature   
    β”‚   └── steps.ts
    └── (video-player)/
        β”œβ”€β”€ video-player.feature    
        └── steps.ts
Enter fullscreen mode Exit fullscreen mode
  • (game)/steps.ts are applied only to game.feature
  • (video-player)/steps.ts are applied only to video-player.feature
  • steps/common.ts are applied to both

The main advantage is that any tool or human can understand paring without reading configuration.
The configuration itself simply defines steps glob without any patterns:

stepDefinitions: 'features/**/*.ts'
Enter fullscreen mode Exit fullscreen mode

Some projects have separate directories for features and steps. For such cases, the rule can be slightly enhanced:

Scoped step definitions are applicable only to features having that scope in the path.

Now the following structure is also possible:

└── features/
    β”œβ”€β”€ steps/
    β”‚   β”œβ”€β”€ common.ts
    β”‚   β”œβ”€β”€ (game).ts
    β”‚   └── (video-player).ts
    β”œβ”€β”€ (game).feature   
    └── (video-player).feature  
Enter fullscreen mode Exit fullscreen mode
  • steps from steps/(game).ts will be applied only to (game).feature, because feature path contains (game)
  • steps from steps/(video-player).ts will be applied only to (video-player).feature, because feature path contains (video-player)
  • steps from steps/common.ts will be applied to both features, because there are no scoped directories in steps path

Such file structure explicitly shows how features are connected to steps.

Conclusion

I think, scoped duplicate steps are reasonable, especially for testing large applications. I haven't seen file-based solutions before and would appreciate any feedback from you. All of you have different projects with unique structure. Feel free to share, how that solution matches your setup.
Thanks in advance and happy testing ❀️

Top comments (2)

Collapse
 
mahesh_prem_cedfb5c9209cc profile image
Mahesh Prem

I don't think the concept should hold true.. cucumber was initially started as documentation/runner. We are trying to say that its purely a technical tool - most people would disagree if so.

Primarily gherkin serves as a easy way to describe steps and provide clarity, but also to replicate scenarios .. as well as orchestrate steps and provide data. Never solely to do efficient test creation.

We are breaking 2 paradigms - requirements clarity + accuracy of organization - by saying same description applies everywhere.Thats not how gherkin itself should be used.

How can I actually treat this..

When I click the `Game_PLAY` button in `GAME` page # <- duplicated step
When I click the `Video_PLAY` button in `Video` page # <- duplicated step
Enter fullscreen mode Exit fullscreen mode

-- GAME/Video second parameter just place holders. no real value.Clarifiers sort of..

and put them in a folder called common steps and import in feature wise steps. Step implementation and even lines of code will be common ..

Collapse
 
vitalets profile image
Vitaliy Potapov

So you mean the better approach is to define common step like this?

When('I click the {string} button in {string}', function (button, page) {
  switch (page) {
     case 'GAME': {
        // game page specific code to click button
     }
     case 'VIDEO': {
        // video page specific code to click button
     }
     case 'SOME_OTHER_PAGE': {
        // ...
     }
  }
});
Enter fullscreen mode Exit fullscreen mode

Isn't it more difficult to maintain such common function for all page?