Reading:
Strategies to simplify your BDD step definitions
Contribute to TestBash Brighton 2025 image
Submit your ideas today or before the 22 December 2024

Strategies to simplify your BDD step definitions

Techniques to boost your efficiency for implementation, writing BDD steps definitions and glue code

 An orange cartoon character with three eyes, dressed in a space helmet, raises its arms joyfully against a backdrop of a purple labyrinth and a starry sky.

If you are a tester, you have probably heard aboutĀ Behaviour-Driven Development, or BDD in short, and the debates around what it is, and how and what it should be used for. Regardless of what we think about the subject, we cannot deny that test automation tools built to support BDD are with us. They are adopted and used widely in the industry, and they will be with us for some time.

Throughout my testing career, a great portion of my test automation activities involved using some kind of BDD test automation framework. Tools like Cucumber, JBehave and other alternatives. As someone who does coding, Iā€™ve always been interested in refactoring to lessen the amount of boilerplate and duplicate code. This results in improved comprehension and minimising the amount of code. This includes reducing boilerplate code in step definition methods and in glue code overall. Simplifying them. Or getting rid of them entirely if possible.

You may be wondering, ā€œWhat is glue code?ā€ On one hand, it consists of step definition methods. Ones that tell the BDD automation framework what to execute when it encounters a Given, When or Then step in a Gherkin feature file. Essentially glueing parts of Gherkin plain text files to executable test automation code. On the other hand, it can be hooks. Methods that execute before or after Gherkin Features or Scenarios.

In this article, Iā€™ll present various ways of simplifying glue code and integrating them closer to the language of your automated tests. In the code examples below Iā€™ll use Cucumber and Java code snippets.

Standard Cucumber step definitions

First, to put you into context, let me demonstrate how a regular Cucumber step definition may look like in Java.

Using regular expression as the step pattern:

@When("^user attempts to log in with (.*) username and (.*) password$")
void userAttemptsToLogInWith(String username, String password) { }

or using Cucumber expression as the step pattern:

@When("user attempts to log in with {string} username and {string} password")
void userAttemptsToLogInWith(String username, String password) { }

The essential parts of this method are:

  • one of theĀ @Given,Ā @When,Ā @Then,Ā @And,Ā @But annotations placed on the method to signal the type of the step, and make the method be recognised as a step definition,
  • the step pattern specified in the annotation: this is how Cucumber maps the step in a Gherkin file to this method,
  • the method arguments that map to the arguments of the Gherkin step.

Now, letā€™s look into the ways of simplifications and alternate usages.

Java 8 lambda expression-based step definitions

With the introduction of lambda expressions in Java 8, a lot has improved and become simpler. It has brought forth alternative ways of implementing applications. This includes Cucumber as well as introducing a new way of defining step definitions using lambda expressions.

For that, you have to use a different dependency, namely cucumber-java8. Once you have this configured, you have to make the following changes in your step definition classes.

The class must implement theĀ io.cucumber.java8.En interface (or one of its language specific variants). With this change, new step type specific methods (Given(),Ā When(), etc.) become available in your step definition class.

Your current step definition methods (or at least the ones that you want to simplify) have to be converted and moved to the class constructor. So, the following method:

class LoginStepDefs {
   @When("user attempts to log in with {string} username and {string} password")
   void userAttemptsToLogInWith(String username, String password) {
       //login actions
   }
}

would become

class LoginStepDefs implements En {
   LoginStepDefs() {
       When("user attempts to log in with {string} username and {string} password",
           (String username, String password) -> {
               //login actions
           });
   }
}

From a readability point of view:

  • This form eliminates theĀ void keyword and the entire method name, making the definition more concise, and quicker to implement.
  • However, although the code is more concise, having several step definitions shoved into the constructor can feel overcrowded and sometimes even more noisy than having them in the old-school method form.

There is some debate on whether the regular method form or the lambda one is better. There are both personal preferences and practical arguments (e.g. dependency injection) put on the table, and there has even beenĀ some discussion on replacing Cucumberā€™s Java8 library with an alternative solution.

Patternless step annotations

The following questions can be made for the step annotations:

  • Do you actually need to explicitly specify a step pattern?
  • Is there an alternative to still have it specified?

Years ago, on a previous project, we had a custom test automation solution and test runner that worked somewhat differently from Cucumber and other BDD frameworks. That solution used its ownĀ @Given,Ā @When, etc. annotations with one main difference: you didnā€™tĀ have to specify the pattern in them. Instead you had to phrase the name of the step definition method in the way it would be used in the Gherkin files.Ā 

For example:

@Given
void userAttemptsToLogInWithXUsernameAndYPasswordOnZ(String username, String password, Server server) { }

You may have noticed the uppercase letters X, Y and Z. They allowed users to parameterise these methods, and it worked as the following:

  • a regular expression was generated from the method name, where X, Y and Z would be replaced with theĀ (.*) capturing group, like this:
^user attempts to log in with (.*) username and (.*) password on (.*)$
  • then in a follow-up step, the framework would go through the list of supported parameter type resolvers and perform the following:
    • it parsed each argument (the content of eachĀ (.*) group) into the corresponding type specified in the methodā€™s argument list,
    • then, it injected the resolved values.

This is sort of the opposite way to how Cucumber step definitions work. There, you specify the actual pattern without the method name, while here you specify the method name as a sort of pattern without specifying the actual pattern.

The advantage of this was that you did not have to specify the pattern in the annotation, only in the method name. This resulted in less coding and less noisy code. But, the step annotations still provided an attribute to specify a custom pattern if the default, generated one was not sufficient.

This also made engineers phrase the names of step definition methods in a way they would be used in the Gherkin files, resulting in clearer steps. It also prevented confusion regarding having a step pattern used with a different method name for no proper reason. Like something like the one below would have caused:

@When("user attempts to log in with {string} username and {string} password")
void authenticate(String username, String password) {
   //login actions
}

Leverage custom IDE integration

Although, using a custom-developed IDE plugin does not necessarily reduce the amount of test code you have, it can at least help increase the level and easiness of code comprehension.

The idea Iā€™ll demonstrate involves a feature called code folding that is a standard one in many IDEs and text editors. Code folding is when an arbitrary range of text in a document becomes hidden (i.e. collapsible and expandable), and is replaced with a custom placeholder text that lets you know a summary of what is under that folded section.

Such a placeholder text may be as simple as an ellipsis (the ā€¦ symbol). This is used mostly when the content, for example a method body, is not relevant at the moment, and it just needs to be hidden.

A Cucumber Java step definition method whose method body is collapsed and hidden, and replaced with an ellipsis. It reads as @When("^user attempts to log in with (.*) username and (.*) password on (.*)$") void userAttemptsToLogInWith(String username, String password, Server server) {...}

Or, it can be actual contextual information that provides the same or almost the same information that the actual unfolded code provides, but in a simpler way. A good example for this is folding anonymous object instantiation in Java to lambda expression style. So, given the following method:

CredentialsProvider getCredentialsFor(String userType) {
   return new CredentialsProvider() {
       @Override
       public Credentials get() {
           return credentialsService.get(userType);
       }
   };
}

Ā 

the folded code will look like this:

It shows the previous Java method but the return statement is folded and reads like CredentialsProvider getCredentialsFor(String userType) { return () -> { return credentialsService.get(userType); }; }

Now that youā€™ve seen a few simple examples of code folding, letā€™s see some ideas on how certain parts of step definition methods may be folded to achieve a better comprehension of them.

Use a step pattern in place of the step definition methodā€™s name

In my experience, when it comes to reading and comprehending a step definition method, people focus on understanding it by reading the step pattern instead of the method name. So why not improve this aspect? Since the method name may be a duplicate of the step pattern, and it cannot be omitted if one uses actual methods, letā€™s try to make it appear in a more concise way.

This involves two steps. First, fold the step pattern in the step annotation into an ellipsis like this:

Shows a version of the original login step definition where the step pattern in the @When annotation is folded, and results in the following displayed in the editor: @When(...) void userAttemptsToLogInAs(UserType userType) { }

This results in a less noisy code, and you still have the information of what type of step (Given, When or Then) this method is for.

Then, if you prefer reading the step pattern, and the step pattern provides more context for the step than the methodā€™s name, you can go one step further. Fold the method name using as placeholder text the step pattern.

Shows a version of the original login step definition where the step pattern in the @When annotation is folded, and results in the following displayed in the editor: @When(...) void "user attempts to log in as {userType}"(UserType userType) { }

If you have the option, you can fold theĀ public andĀ void keywords as well, so you end up with the following method ā€œsignatureā€:

Shows a version of the original login step definition where the step pattern in the @When annotation is folded, and results in the following displayed in the editor: @When(...) void "user attempts to log in as {userType}"(UserType userType) { }

Of course, if you want to, you can go further, or in a completely different direction, with the customization of this folding. This is up to your personal or project preferences.

Dynamic resolution of steps

Letā€™s say you have steps that have a clear formalisable format. You donā€™t want to deal with implementing separate step definitions for each of them because it would be an unnecessary duplication.

One thing you can do in this situation, and what we did on a previous project, is using dynamic parsing and resolution of steps. This eliminated the need to implement actual step definition methods for them. We mostly used it to build validation steps for web UI test automation, like the ones below. (Screaming snake case parts reference SeleniumĀ WebElements andĀ Bys.)

  • Then theĀ TITLE of theĀ RECOMMENDED_BOOKS module should beĀ "Recommended books"
  • ThenĀ theĀ TITLE of the first item of theĀ RECOMMENDED_BOOKS module should beĀ "Atomic Habits"
  • ThenĀ the secondĀ AUTHOR of the first item of theĀ RECOMMENDED_BOOKS module should beĀ ā€œNooneā€

As you may see, it kind of works in a backwards order. If you take the last example:

  • It locates theĀ Recommended Books module on the page the scenario is currently on.Ā RECOMMENDED_BOOKS here is mapped to a list ofĀ WebElements.
  • It gets theĀ first item from that list, which is the element of an actual book.
  • It gets the list ofĀ AUTHORs as strings.Ā AUTHOR here is mapped to aĀ By object.
  • Then, it gets theĀ second item from the list of authors.
  • Finally, it compares the found value to the expected one specified in the ā€˜should be ā€œNooneā€ā€™ part.

All of this was made possible by the implementation of a common parser logic. Once it was put in place, no coding was required to explicitly implement this kind of Gherkin steps. The only coding involved in this area were either bug fixes, improvements to the parser logic or extending the corresponding page objects.

Now, of course one could argue how readable these steps are, or whether they could be replaced with visual testing. But, at that time, it provided a clear structure and format. Customisation of underlying page elements, choosing elements by index, and other goodies too. For us it was a nice way of implementing validation with a minimal amount of coding.

Use frameworks with pre-implemented step libraries

One way of minimising the amount of glue code is getting rid of them entirely. This may be achieved for example by using libraries that provide pre-implemented steps for many common or not so common tasks.

They may also have different templating solutions and expression languages to customise steps and actions with dynamic input data, like various types of request bodies and headers for sending HTTP requests in API tests.

Iā€™m only including brief introductions of some libraries below, just to give you an idea where to begin, then Iā€™ll let you delve into them if you are interested.

Framework

Summary from the frameworkā€™s documentation

Scenarios are implemented as ...

Karate ā€œKarate is the only open-source tool to combine API test-automation, mocks, performance-testing and even UI automation into a single, unified framework. The syntax is language-neutral ā€¦ in a simple, readable syntax - carefully designed for HTTP, JSON, GraphQL and XML. And you can mix API and UI test-automation within the same test script.ā€ Gherkin Features with Karateā€™s own language parser (instead of Cucumbers)
Vividus

ā€œVIVIDUS is a test automation tool that offers already implemented solution for testing of the most popular application types.ā€Ā 

Ā 

This includes database, API (Application Programming Interface) and UI (User Interface) testing as well.

JBehave Stories
Citrus YAKS

ā€œYAKS is a framework to enable Cloud Native BDD testing on Kubernetes! Cloud Native here means that your tests execute as Kubernetes PODs.

Ā 

As a framework YAKS provides a set of predefined Cucumber steps which help you to connect with different messaging transports (Http REST, JMS, Kafka, Knative eventing) and verify message data with assertions on the header and body content.ā€

Cucumber Gherkin Features

Take scenario development to the code level

Although this approach doesnā€™t get rid of step definition methods, it simplifies tests from a different, sort of the opposite, perspective of what I introduced in the previous section. Instead of implementing your tests in Gherkin or similar files, and having to bother with step definition implementations, you implement tests as actual code in a Gherkin-like DSL (Domain Specific Language). This way you donā€™t have to touch any actual Gherkin file. That is what theĀ JGiven framework aims to achieve, or at least how it fits into the topic of this article.

With applying some basic test configuration, and extending some base classes, you can implement ā€œGherkinā€ steps and scenarios in regular JUnit, TestNG or Spock test methods. Thus, you also have proper and direct access to the assertion, mocking, etc. libraries that you use, and your test methods would read close to actual Gherkin scenarios with test reports generated accordingly.

The following one is a Given step with a more granular implementation (see the corresponding section in the JGiven documentation):

@Test
public void validate_recipe() {
   given().the().ingredients()
       .an().egg()
       .some().milk()
       .and().the().ingredient("flour");


   //other stepsā€¦
}

Or, if you take a more advanced scenario, the JUnit 5 test method below, it executes a parameterised test with multiple different input data. It essentially simulates the execution of a GherkinĀ Scenario Outline with a set of input data itsĀ Examples table would receive.

This example is from theĀ Parameterized Scenarios section of the JGiven documentation, altered for conciseness and to use the more commonly used JUnit 5 parameterised test approach.

@ParameterizedTest
@CsvSource({ "1, 1", "0, 2", "1, 0" })
public void coffee_is_not_served(int coffees, int euros) {
   given().a_coffee_machine()
       .and().the_coffee_costs_$_euros(2)
       .and().there_are_$_coffees_left_in_the_machine(coffees);


   when().I_insert_$_one_euro_coins(euros)
       .and().I_press_the_coffee_button();


   then().I_should_not_be_served_a_coffee();
}

This test method would generate the following report, so you would still have proper test reports and living documentation that you can share with various stakeholders.

Shows JGiven's HTML test report for the previous test method with a collapsible header called "Serve Coffee Coffee is not served". The steps in the tested scenario are: Given a coffee machine And he coffee costs 2 euros And there are <coffee> coffees left in the machine When I insert <euros> one euor coins And I press the coffee button Then I should nt be served a coffee  Underneath it shows a table with the various input data and the status of the test under a header called Cases. The first column shows the ids of the tests as 1, 2 and 3. The rest of the columns read as coffees, euros, Status. The test case rows read: 1 coffees, 1 euros, passed 0 coffees, 2 euros, passed 1 coffees, 0 euros, passed

We can take this example one step further by applying some IDE plugin magic on it. Using custom-developed code to fold in this case, you could bring your test code even closer to how an actual Gherkin scenario would read. Something like this:

Shows the previous test method called coffee_is_not_served but with its body folded as  @ParameterizedTest @CsvSource({ "1, 1", "0, 2", "1, 0" }) public void coffee_is_not_served( int coffees, int euros ) { Given a coffee maching And the coffee costs 2 euros And there are <coffees> coffees left in the machine;  When I insert <euros> one euro coins And I press the coffee button;  Then I should not be served a coffee; }

Please note that this code folding is not from an existing IDE plugin. It was made specifically for this article for demonstration purposes.

Donā€™t use BDD frameworks at all

Whether you should implement your tests as BDD scenarios and use a test framework that supports that, depends on the project domain and resources, application type and other aspects.

One argument I have heard many times is the fact that glue code adds an unnecessary layer of abstraction, since it has to be thin, and, in an ideal case, should only delegate test execution to the actual underlying test code.

It is simply worth keeping in mind that you can always opt to implement your tests as regular JUnit or TestNG ones.

A couple of options to choose from

I have described a couple of options that you can utilise in your test suites and during your test automation. They have different learning curves and require different sets of knowledge to apply, so Iā€™m not advocating for any specific solution above. Iā€™m simply hoping I could show you some interesting ways and alternatives to spark your imagination about how you could simplify your or your teamā€™s life when you are using BDD based test automation.

If you think I missed any other ways of simplification, please let me know on The Club (link required).

Resources

For more information

TamƔs Balog
Freelance IDE plugin developer
I develop JetBrains IDE plugins for test automation tools like WireMock, Mockito and others. I'm also a former QA and Test Automation Engineer of more than 10 years.
Comments
Contribute to TestBash Brighton 2025 image
Submit your ideas today or before the 22 December 2024
Explore MoT
Episode Seven: The community's guide to Continuous Quality image
The trend towards continuous quality, rather than shift left
MoT Foundation Certificate in Test Automation
Unlock the essential skills to transition into Test Automation through interactive, community-driven learning, backed by industry expertise
This Week in Testing
Debrief the week in Testing via a community radio show hosted by Simon Tomes and members of the community
Subscribe to our newsletter
We'll keep you up to date on all the testing trends.