Guide: Cucumber + Java

Tomcat

Professional
Messages
2,656
Reputation
10
Reaction score
647
Points
113
Unfortunately, there is no magic formula for developing high-quality software, but it is clear that testing improves the quality of software, and test automation improves the quality of testing itself.

In this article we will look at one of the most popular frameworks for test automation using the BDD approach - Cucumber. Let's also see how it works and what tools it provides.
Cucumber was originally developed by the Ruby community, but over time it was adapted for other popular programming languages. In this article we will look at how Cucumber works in Java.

Gherkin​


BDD tests are simple text, in human language, written in the form of a story (scenario) that describes some behavior.

Cucumber uses Gherkin notation to write tests, which defines the test structure and a set of keywords. The test is written to a file with the *.feature extension and can contain one or more scenarios.

Let's look at an example test in Russian using Gherkin:

Code:
# language: en
@all
Function: Bank Card Authentication
 The ATM must ask the user for the PIN code of the bank card
 The ATM should issue a warning if the user has entered an incorrect PIN
 Authentication is successful if the user entered the correct PIN

 Background:
 Let’s say a user inserts a bank card into an ATM
 And the ATM displays a message about the need to enter a PIN code

 @correct
 Scenario: Successful Authentication
 If the user enters the correct PIN code
 Then the ATM displays the menu and the amount of available money in the account

 @fail
 Scenario: Incorrect Authentication
 If the user enters an incorrect PIN code
 Then the ATM displays a message that the entered PIN code is incorrect

As can be seen from the example, the scripts are described in simple non-technical language, thanks to which any project participant can understand and write them.

Pay attention to the structure of the script:
1. Get the initial state of the system;
2. Do something;
3. Get the new system state.

In the example, the keywords are highlighted in bold. Below is a complete list of keywords:
  1. Given, Suppose, Let – are used to describe a preliminary, previously known state;
  2. When, If – are used to describe key actions;
  3. And, Moreover, Also – are used to describe additional preconditions or actions;
  4. Then, Then – are used to describe the expected result of the action performed;
  5. But, A – are used to describe an additional expected result;
  6. Function, Functional, Property – used to name and describe the functionality being tested. The description can be multi-line;
  7. Scenario – used to indicate a scenario;
  8. Background, Context - used to describe the actions performed before each script in the file;
  9. Script structure, Examples - used to create a script template and a table of parameters passed to it.

The keywords listed in points 1-5 are used to describe the script steps; Cucumber does not technically differentiate between them. You can use the * symbol instead , but this is not recommended. These words have a purpose, and they were chosen for that purpose.

List of reserved characters:
# – denotes comments;
@ – tags scripts or functionality;
| – divides data in tabular format;
""" – frames multi-line data.

The script begins with the line # language: ru. This line indicates to Cucumber that the Russian language is used in the script. If you do not specify it, the framework, when it encounters Russian text in the script, will throw a LexingError exception and the test will not run. The default language is English.

Simple project​


The Cucumber project consists of two parts: text files with descriptions of scenarios (*.feature) and files with implementation of steps in a programming language (in our case, *.java files).

To create a project, we will use the Apache Maven project build automation system.
First of all, let's add cucumber to the Maven dependency:

Code:
<dependency>
    <groupId>info.cukes</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>1.2.4</version>
</dependency>

To run tests we will use JUnit (can be launched via TestNG), for this we will add two more dependencies:
Code:
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>
<dependency>
    <groupId>info.cukes</groupId>
    <artifactId>cucumber-junit</artifactId>
    <version>1.2.4</version>
</dependency>

The cucumber-junit library contains the cucumber.api.junit.Cucumber class, which allows you to run tests using the JUnit RunWith annotation. The class specified in this annotation defines how to run tests.

Let's create a class that will be the entry point for our tests.

Code:
import cucumber.api.CucumberOptions;
import cucumber.api.SnippetType;
import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(
        features = "src/test/features",
        glue = "ru.savkk.test",
        tags = "@all",
        dryRun = false,
        strict = false,
        snippets = SnippetType.UNDERSCORE,
// name = "^Successful|Successful.*"
)
public class RunnerTest {
}

Please note that the class name must end with Test, otherwise the tests will not run.

Let's look at the Cucumber options:
  1. features – path to the folder with .feature files. The framework will search for files in this and all child folders. You can specify multiple folders, for example: features = {"src/test/features", "src/test/feat"};
  2. glue – a package containing classes with the implementation of steps and hooks. You can specify several packages, for example, like this: glue = {"ru.savkk.test", "ru.savkk.hooks"};
  3. tags – filter for running tests by tags. The list of tags can be separated by commas. The ~ symbol excludes a test from the list of tests to run, for example ~@fail;
  4. dryRun – if true, then immediately after running the test, the framework checks whether all test steps have been developed; if not, it issues a warning. If false, a warning will be issued when an undeveloped step is reached. Default is false.
  5. strict – if true, then if an undeveloped step is encountered, the test will stop with an error. False—undeveloped steps are skipped. Default is false.
  6. snippets – indicates in what format the framework will offer a template for unrealized steps. Available values: SnippetType.CAMELCASE, SnippetType.UNDERSCORE.
  7. name – filters launched tests by names that satisfy the regular expression.

To filter launched tests, you cannot use the tags and name options simultaneously.

Creating a “feature”

In the src/test/features folder, create a file with a description of the functionality being tested. Let's describe two simple scenarios for withdrawing money from an account - successful and unsuccessful.

# language: en
@withdrawal
Function: Withdrawing money from the account

@Success
Scenario: Successful withdrawal of money from an account
Given that there are 120,000 rubles in the user account
When a user withdraws 20,000 rubles from the account
Then the user’s account has 100,000 rubles

@fail
Scenario: Withdrawing money from account - not enough money
Given that there are 100 rubles on the user account
When a user withdraws 120 rubles from the account
Then the warning “There is not enough money in your account” appears.

Let's launch

Let's try to launch RunnerTest with the following settings:

Code:
@RunWith(Cucumber.class)
@CucumberOptions(
        features = "src/test/features",
        glue = "ru.savkk.test",
        tags = "@withdrawal",
        snippets = SnippetType.CAMELCASE
)
public class RunnerTest {
}

The test result appeared in the console:

Code:
Undefined scenarios:
test.feature:6 # Scenario: Successful withdrawal of money from the account
test.feature:12 # Scenario: Withdrawal of money from account - not enough money

2 Scenarios (2 undefined)
6 Steps (6 undefined)
0m0,000s

You can implement missing steps with the snippets below:

@Given("^user account has (\\d+) rubles$")
public void onUserAccountHasRubles(int arg1) throws Throwable {
 // Write code here that turns the phrase above into concrete actions
 throw new PendingException();
}

@When("^user withdraws (\\d+) rubles$ from the account")
public void userWithdrawsRublesAccount(int arg1) throws Throwable {
 // Write code here that turns the phrase above into concrete actions
 throw new PendingException();
}

@Then("^a warning appears \"([^\"]*)\"$")
public void appearsWarning(String arg1) throws Throwable {
 // Write code here that turns the phrase above into concrete actions
 throw new PendingException();
}

Cucumber did not find an implementation of the steps and offered its own templates for development.
Let's create the MyStepdefs class in the ru.savkk.test package and transfer the methods proposed by the framework to it:

Code:
import cucumber.api.PendingException;
import cucumber.api.java.ru.*;

public class MyStepdefs {

 @Given("^user account has (\\d+) rubles$")
 public void onUserAccountHasRubles(int arg1) throws Throwable {
 // Write code here that turns the phrase above into concrete actions
 throw new PendingException();
 }

 @When("^user withdraws (\\d+) rubles$ from the account")
 public void userWithdrawsRublesAccount(int arg1) throws Throwable {
 // Write code here that turns the phrase above into concrete actions
 throw new PendingException();
 }

 @Then("^a warning appears \"([^\"]*)\"$")
 public void appearsWarning(String arg1) throws Throwable {
 // Write code here that turns the phrase above into concrete actions
 throw new PendingException();
 }
}

When you run a test, Cucumber goes through the script step by step. Taking a step, it separates the keyword from the step description and tries to find in the Java classes of the package specified in the glue option an annotation with a regular expression that matches the description. Having found a match, the framework calls the method with the found annotation. If more than one regular expression matches the step description, the framework throws an error.

As mentioned above, for Cucumber there is technically no difference in the keywords describing the steps, this is also true for the annotation, for example:

Code:
@When("^user withdraws (\\d+) rubles$ from the account")

And

Code:
@Then("^user withdraws (\\d+) rubles$ from the account")

for the framework are the same.

What is written in parentheses in regular expressions is passed to the method as an argument. The framework independently determines what needs to be passed from the script to the method as an argument. These numbers are (\\d+). And the text escaped in quotes is \"([^\"]*)\". These are the most common arguments passed.

The table below shows the elements used in regular expressions:
regular expressions:
ExpressionDescriptionCompliance
.Any one character (except line break)F
2
j
.*0 or more any characters (except line breaks)Abracadabra
789-160-87,
.+One or more any characters (except line break)Everything related to the previous one, except empty
lines.
.{2}Any two characters (except line break)Ff
22
$x
JJ
.{1,3}Any one to three characters (except line breaks)LJJ
Ooh
!
^Start of line anchor^aaa matches aaa
^aaa corresponds to aaabbb
^aaa does not match bbbaaa
$End of line anchoraaa$ matches aaa
aaa$ does not match aaabbb
aaa$ corresponds to bbbaaa
\d*
[0-9]*
Any number (or nothing)12321

5323
\d+
[0-9]+
Any numberEverything related to the previous one, except for the empty line.
\w*Any letter, number, or underscore (or nothing)_we
_1ee
Gfd4
\sSpace, tab, or line break\t,\r
or \n
"[^\"]*"Any character (or nothing) in quotes"aaa"
""
"3213dsa"
?Makes a character or group of characters optionalabc?
corresponds to ab
or abc, but not b or bc
|Logical ORaaa|bbb
matches aaa
or bbb, but not aaabbb
()Group. In Cucumber, the group is passed to the step definition as an argument.(\d+) rubles corresponds to 10 rubles, with 10 being passed to the step method as an argument
(?: )Not transmitted group.
Cucumber does not treat group as an argument.
(\d+) (?: rubles|rubles) corresponds to 3 rubles, while 3 is passed to the method,
but “ruble” - no.

Passing collections as arguments​


A situation often arises when it is necessary to transfer a set of similar data - collections - from a script to a method. For a similar task, Cucumber has several solutions:

  1. The framework by default wraps comma-separated data in an ArrayList:

    Given in the menu the following items are available: File, Edit, About the program

    Code:
    @Given("^items available in the menu (.*)$")
    public void inMenuAvailableItems(List<String> arg) {
    // do something
    }

    To replace the delimiter, you can use the Delimiter annotation:

    Given in the menu the items File and Edit and About the program are available

    Code:
    @Given("^the menu items are available (.+)$")
    public void inMenuAvailableItems(@Delimiter(" and ") List<String> arg) {
    // do something
    }
  2. Data written as a table with one column can also be wrapped in an ArrayList by Cucumber:

    Code:
    Given menu items available
    | File |
    | Edit |
    | About the program |

    Code:
    @Given("^items are available in the menu$")
    public void inMenuAvailableItems(List<String> arg) {
    // do something
    }
  3. Cucumber can wrap data written into a table with two columns into an associative array, where the data from the first column is the key, and the data from the second is the data:

    Code:
    Given menu items available
    | File | true |
    | Edit | false |
    | About the program | true |

    Code:
    public void inMenuAvailableItems(Map<String, Boolean> arg) {
    // do something
    }
  4. Transferring data in the form of a table with a large number of columns is possible in two ways:
    • DataTable

      Code:
      Given menu items available
      | File | true | 5 |
      | Edit | false | 8 |
      | About the program | true | 2 |
      
      @Given("^items are available in the menu$")
      public void inMenuAvailableItems(DataTable arg) {
      // do something
      }

      DataTable is a class that emulates a tabular representation of data. It has a large number of methods for accessing data. Let's look at some of them:

      Code:
      public <K,V> List<Map<K,V>> asMaps(Class<K> keyType,Class<V> valueType)

      Converts a table to a list of associative arrays. The first row of the table is used to name the keys, the rest are used as values:

      Code:
      Given menu items available
      | Title | Available | Number of submenus |
      | File | true | 5 |
      | Edit | false | 8 |
      | About the program | true | 2 |[/CODE]

      Code:
      @Given("^items are available in the menu$")
      public void inMenuAvailableItems(DataTable arg) {
      List<Map<String, String>> table = arg.asMaps(String.class, String.class);
      System.out.println(table.get(0).get("Title"));
      System.out.println(table.get(1).get("Title"));
      System.out.println(table.get(2).get("Title"));
      }

      This example will output to the console:

      Code:
      File
      Edit
      About the program

      Code:
      public <T> List<List<T>> asLists(Class<T> itemType)

      The method converts the table to a list of lists:

      Code:
      Given menu items available
      | File | true | 5 |
      | Edit | false | 8 |
      | About the program | true | 2 |

      Code:
      @Given("^items are available in the menu$")
      public void inMenuAvailableItems(DataTable arg) {
      List<List<String>> table = arg.asLists(String.class);
      System.out.print(table.get(0).get(0) + " ");
      System.out.print(table.get(0).get(1) + " ");
      System.out.println(table.get(0).get(2) + " ");
      
      System.out.print(table.get(1).get(0) + " ");
      System.out.print(table.get(1).get(1) + " ");
      System.out.println(table.get(1).get(2) + " ");
      }

      The following will be displayed on the console:

      Code:
      File true 5
      Edit false 8

      Code:
      public List<List<String>> cells(int firstRow)

      This method does the same as the previous method, except that it is impossible to determine what type of data is in the table; it always returns a list of strings - List. The method takes the first line number as an argument:

      Code:
      Given menu items available
      | File | true | 5 |
      | Edit | false | 8 |
      | About the program | true | 2 |

      Code:
      @Given("^items are available in the menu$")
      public void inMenuAvailableItems(DataTable arg) {
      List<List<String>> table = arg.cells(1);
      System.out.print(table.get(0).get(0) + " ");
      System.out.print(table.get(0).get(1) + " ");
      System.out.println(table.get(0).get(2) + " ");
      
      System.out.print(table.get(1).get(0) + " ");
      System.out.print(table.get(1).get(1) + " ");
      System.out.println(table.get(1).get(2) + " ");
      }

      The method will output to the console:

      Code:
      Edit false 8
      About true 2
    • Class
      Cucumber can create objects from tabular data passed from a script. There are two ways to do this.

      Let's create a Menu class for example:

      Code:
      public class Menu {
      private String title;
      private boolean isAvailable;
      private int subMenuCount;
      
      public String getTitle() {
      return title;
      }
      
      public boolean getAvailable() {
      return isAvailable;
      }
      
      public int getSubMenuCount() {
      return subMenuCount;
      }
      }

      For the first method, we write the step in the script as follows:

      Given menu items available
      | title | isAvailable | subMenuCount |
      | File | true | 5 |
      | Edit | false | 8 |
      | About the program | true | 2 |

      Implementation:

      Code:
      @Given("^items are available in the menu$")
      public void inMenuAvailableItems(List<Menu> arg) {
      for (int i = 0; i < arg.size(); i++) {
      System.out.print(arg.get(i).getTitle() + " ");
      System.out.print(Boolean.toString(arg.get(i).getAvailable()) + " ");
      System.out.println(Integer.toString(arg.get(i).getSubMenuCount()));
      }
      }

      Console output: The framework creates a linked list of objects from a table with three columns. The first row of the table should indicate the names of the fields of the class being created. If a field is not specified, it will not be initialized. For the second method, we reduce the script step to the following form:

      Code:
      File true 5
      Edit false 8
      About true 2

      Code:
      Given menu items available
      | title | File | Edit | About the program |
      | isAvailable | true | false | true |
      | subMenuCount | 5 | 8 | 2 |

      And in the step description argument we use the @Transpose annotation.

      Code:
      @Given("^items are available in the menu$")
      public void inMenuAvailableItems(@Transpose List<Menu> arg) {
      // do something
      }

      Cucumber, as in the previous example, will create a linked list of objects, but, in this case, the names of the fields are written in the first column of the table.
  5. Multiline Arguments

    To pass multiline data to a method argument, it must be escaped with three double quotes:

    Code:
    Then the form with the text is displayed
    """
    A one-time password has been sent to your phone number.
    To confirm the payment, you must enter the received
    one-time password.
    """

    Data comes to the method in the form of an object of the String class:

    Code:
    @Then("^form appears with text$")
    public void displayedFormText(String expectedText) {
    // do something
    }

Date​


The framework automatically converts the data from the script to the data type specified in the method argument. If this is not possible, it throws a ConversionException. This is also true for the Date and Calendar classes. Let's look at an example:

Code:
The document creation date is given as 05/04/2024

Code:
@Given("^document creation date (.+)$")
public void DocumentCreation date (Date arg) {
 // do something
}

Everything worked great, Cucumber converted 05/04/2024 into a Date object with the value “Thu May 04 00:00:00 EET 2024”.

Let's look at another example:

Code:
The document creation date is given as 05/04/2024

Code:
@Given("^document creation date (.+)$")
public void DocumentCreation date (Date arg) {
 // do something
}

Having reached this step, Cucumber threw an exception:

Code:
cucumber.deps.com.thoughtworks.xstream.converters.ConversionException: Couldn't convert "04-05-2017" to an instance of: [class java.util.Date]

Why did the first example work and the second not?

The fact is that Cucumber has built-in support for date formats that are sensitive to the current locale. If you need to write a date in a format that differs from the format of the current locale, you need to use the Format annotation:

Code:
The document creation date is given as 05/04/2024

Code:
@Given("^document creation date (.+)$")
public void DocumentCreation date (@Format("dd-MM-yyyy") Date arg) {
 // do something
}

Script structure​


There are times when it is necessary to run a test several times with a different set of data, in such cases the “Scenario Structure” construct comes to the rescue:

Code:
# language: en
@withdrawal
Function: Withdrawing money from the account

 @success
 Scenario structure: Successful withdrawal of money from an account
 Given that the user's account contains <initially> rubles
 When a user withdraws <withdrawn> rubles from the account
 Then the user’s account has <remaining> rubles

 Examples:
 | originally | taken | left |
      | 10000      | 1     | 9999     |
      | 9999       | 9999  | 0        |

The essence of this design is that data from the Examples table is inserted into the places indicated by the <> symbols. The test will be run one by one for each row from this table. The names of the columns must match the names of the places where data is inserted.

Using Hooks​


Cucumber supports hooks—methods that run before or after a script. The Before and After annotations are used to denote them. The class with hooks must be in the package specified in the framework options. Example class with hooks:

Code:
import cucumber.api.java.After;
import cucumber.api.java.Before;

public class Hooks {
    @Before
    public void prepareData() {
//prepare data
 }

 @After
 public void clearData() {
 //clear data
 }
}

A method with the Before annotation will be run before each script, After - after.

Execution order​


Hooks can be specified in the order in which they will be executed. To do this, you need to specify the order parameter in the annotation. By default, the value of order is 10000.

For Before, the lower this value, the earlier the method will be executed:

Code:
@Before(order = 10)
public void connectToServer() {
 //connect to the server
}

@Before(order = 20)
public void prepareData() {
 //prepare data
}

In this example, the connectToServer() method will be executed first, then prepareData().

After works in reverse order.

Tagging​


In the value parameter you can specify script tags for which hooks will be executed. The ~ symbol means "except". Example:

Code:
@Before(value = "@correct", order = 30)
public void connectToServer() {
 //do something
}

@Before(value = "~@fail", order = 20)
public void prepareData() {
 //do something
}

The connectToServer method will be executed for all scripts with the correct tag , the prepareData method for all scripts except for scripts with the fail tag .

Scenario class​


If you specify an object of the Scenario class as an argument in the definition of a hook method, then in this method you can find out a lot of useful information about the running script, for example:

Code:
@After
public void getScenarioInfo(Scenario scenario) {
 System.out.println(scenario.getId());
 System.out.println(scenario.getName());
 System.out.println(scenario.getStatus());
 System.out.println(scenario.isFailed());
 System.out.println(scenario.getSourceTagNames());
}

For the script:

Code:
# language: ru
@all
Function: Bank Card Authentication
 The ATM must ask the user for the PIN code of the bank card
 The ATM should issue a warning if the user has entered an incorrect PIN code
 Authentication is successful if the user entered the correct PIN code

 Background:
 Let’s say a user inserts a bank card into an ATM
 And the ATM displays a message about the need to enter a PIN code

 @correct
 Scenario: Successful Authentication
 If the user enters the correct PIN code
 Then the ATM displays the menu and the amount of available money in the account

Will output to the console:

Code:
bank-card-authentication;successful-authentication
Successful authentication
passed
false
[@correct, @all]

Finally​


Cucumber is a very powerful and flexible framework that can be used in conjunction with many other popular tools. For example, with Selenium, a framework for automating web applications, or Yandex.Allure, a library that allows you to create convenient reports.
Good luck with automation everyone.
 
Top