Cucumber in Kotlin

Daniel Delimata
7 min readJun 15, 2023

--

Welcome to my course of Cucumber framework for beginners. In this post we will go through setting up the project in Kotlin. The project will be based on Selenium and the results will be reported by Allure reporting framework.

If you need some more general information about Cucumber and Gherkin please read the introduction.

In my series, I present how to efficiently organize code in various languages. When working with BDD, it is important to remember that coding is the final step. One should never start with coding. BDD begins with a discussion about functionalities, use cases, and examples. After this discussion, a feature description in terms of Given-When-Then is created. Such a description should not focus on webpages. Please see my another post on this topic.

LANGUAGE AND IDE

Kotlin is a modern, statically-typed programming language that runs on the Java Virtual Machine (JVM). It is concise, expressive, and can be used for both backend and frontend development.

To edit Kotlin code, I recommend Idea IntelliJ. Visual Studio Code is also a possible choice, but this post is written from IntelliJ perspective.

WHAT DO WE WANT TO TEST?

Our test page is pretty simple (the same for the whole course). It contains several typical elements of UI forms:

  • text field (input),
  • dropdown,
  • checkbox,
  • table with dynamic content,
  • text element with dynamic content.

The URL address of this page is: https://danieldelimata.github.io/sample-page/.

BEFORE WE START

At first make sure that you have the Java 8 JDK (also known as 1.8). You can do it by running javac -version on the command line. If you don't have version 1.8 or higher, install the JDK

Next necessary thing is IntelliJ Community Edition. You can download it from Jetbrains site. The Kotlin plugin is bundled with the whole IDE.

Once you have installed IDE, you can create a new Kotlin project.

Chose “New project”. In the form select location of your project on your hard drive. As a language select “Kotlin” and as a build system “Gradle”.

New Project window

After successfully setting up the project, you should see the following build.gradle file.

build.gradle file

SETTING UP THE DEPENDENCIES OF THE PROJECT

The build.gradle file contains a list of project dependencies along with their versions. This allows for easy updating of all required packages and tools.

Let us edit the build.gradle file and add the necessary dependencies. Our file will look as follows.

plugins {
id 'org.jetbrains.kotlin.jvm' version '1.8.0'
id 'io.qameta.allure' version '2.8.1'
}

group = 'eu.delimata'
version = '1.0-SNAPSHOT'

repositories {
mavenCentral()
}

ext {
allureVersion = '2.16.1'
assertJVersion = '3.21.0'
cucumberVersion = '6.10.0'
junitVersion = '4.13.1'
kotlinVersion = '1.5.31'
lombokVersion = '1.18.18'
seleniumVersion = '4.10.0'
slf4jVersion = '2.0.0-alpha4'
}

dependencies {
testImplementation group: 'org.seleniumhq.selenium', name: 'selenium-java', version: "${seleniumVersion}"
testImplementation group: 'org.assertj', name: 'assertj-core', version: "${assertJVersion}"
testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: "${slf4jVersion}"
testImplementation group: 'junit', name: 'junit', version: "${junitVersion}"
testImplementation group: 'io.cucumber', name: 'cucumber-junit', version: "${cucumberVersion}"
testImplementation group: 'io.cucumber', name: 'cucumber-java', version: "${cucumberVersion}"
testImplementation group: 'io.qameta.allure', name: 'allure-cucumber6-jvm', version: "${allureVersion}"
testImplementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: "${kotlinVersion}"
testImplementation group: 'org.jetbrains.kotlin', name: 'kotlin-test-junit', version: "${kotlinVersion}"
}

defaultTasks 'clean','test'

Now, we have to place files into the appropriate directories. First, of course, we will place the feature files. We will create the features directory inside the resources directory (as seen in the screenshot above).

features/f01.feature:

Feature: F01 Searching - Clearing of searching criteria

User story:
* As a user of Customers page
* I want to be able to clear searching criteria
* in order to quickly type new criteria

Acceptance criteria:
* After clearing, search criteria and summary should be as in the very beginning.

Scenario Outline: Clearing of searching criteria
Given the user is on the page
When the user enters the value "<Search>" in the text-input
And the user selects value "<Column>" in the drop-down
And the user sets case sensitivity switch to "<Case>"
And the user clears filters
Then the user should see that search criteria are cleared
And the user should see that the search result summary is as in the very beginning

Examples:
| Search | Column | Case |
| Alice | Name | True |
| alice | Name | False |

To be able to run our test, we have to create a runner and Page Objects classes. Let us create the following directories and classes. Obviously, you can choose your own names for packages and classes.

TestRunner1.kt

import io.cucumber.junit.Cucumber
import io.cucumber.junit.CucumberOptions
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.runner.RunWith
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions
import java.util.*


@RunWith(Cucumber::class)
@CucumberOptions(
features = ["src/test/resources/features"],
glue = ["eu.delimata.stepdefinitions"],
snippets = CucumberOptions.SnippetType.CAMELCASE,
tags = "not @ignore",
plugin = [
"junit:build/junit",
"json:build/cucumber.json",
"io.qameta.allure.cucumber6jvm.AllureCucumber6Jvm",
"html:build/cucumber-html-report"
],
monochrome = true
)

class TestRunner1 {

companion object {
var driver: WebDriver? = null

@JvmStatic
@BeforeClass
fun openTheBrowser(): Unit {
val options = ChromeOptions()
.addArguments("--start-maximized")
.addArguments("--headless")
.addArguments("--remote-allow-origins=*")
driver = ChromeDriver(options)
}

@JvmStatic
@AfterClass
fun closeTheBrowser(): Unit {
driver?.quit()
}
}
}

AbstractPageObject.kt

package eu.delimata.pages

import org.openqa.selenium.WebDriver
import org.openqa.selenium.support.PageFactory
import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory

open class AbstractPageObject(private var driver: WebDriver?) {
init {
val ajaxElementLocatorFactory = AjaxElementLocatorFactory(driver, 1)
PageFactory.initElements(ajaxElementLocatorFactory, this)
}
}

CustomersPage.kt

package eu.delimata.pages

import eu.delimata.TestRunner1
import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.support.CacheLookup
import org.openqa.selenium.support.FindBy
import org.openqa.selenium.support.ui.Select

class CustomersPage() : AbstractPageObject(TestRunner1.driver) {

private var driver: WebDriver? = TestRunner1.driver

@FindBy(id = "clear-button")
@CacheLookup
private val clickToClearFilters: WebElement? = null

@FindBy(id = "search-input")
@CacheLookup
private val searchInput: WebElement? = null

@FindBy(id = "search-column")
@CacheLookup
private val searchColumn: WebElement? = null

@FindBy(id = "match-case")
@CacheLookup
private val matchCase: WebElement? = null

@FindBy(id = "table-resume")
private val summary: WebElement? = null

@FindBy(id = "search-slogan")
private val searchTerm: WebElement? = null

@FindBy(xpath = "//table")
private val searchResultsTable: WebElement? = null

fun clickClearFiltersButton(): CustomersPage {
clickToClearFilters!!.click()
return this
}

fun setSearchInput(searchInput: String?): CustomersPage {
this.searchInput!!.sendKeys(searchInput)
return this
}

fun setSearchColumnDropDownListField(value: String?): CustomersPage {
Select(searchColumn).selectByVisibleText(value)
return this
}

fun setMatchCaseCheckboxField(value: Boolean): CustomersPage {
if (value != matchCase!!.isSelected) {
matchCase.click()
}
return this
}

fun open(): CustomersPage {
val pageUrl = "https://danieldelimata.github.io/sample-page/"
driver!!.get(pageUrl)
return this
}

val summaryText: String
get() = summary!!.text
val searchTermText: String
get() = searchTerm!!.getAttribute("innerText")
val searchInputText: String
get() = searchInput!!.getAttribute("innerText")
val searchResultsTableText: String
get() = searchResultsTable!!.text
}

The above class stores the code related to the UI itself only. It should not contain methods related to tests. Where should they be placed? If you read the introduction (or tried Cucumber with another programming language), then you probably know that such a place should be a class responsible for step definitions (also called glue).

In various programming languages, you can generate Cucumber snippets by running your tests without the glue code. We can execute our test using the following command or via IntelliJ GUI.

gradle test

The output of such a command is rather concise and does not contain snippets. To get such snippets, you have to run tests with the --info option.

gradle test --info

Unfortunately, snippets that can be created by Cucumber are not very helpful directly because they are in Java (not in Kotlin). Fortunately, IntelliJ offers conversion from Java to Kotlin every time you paste the code.

At this moment, the main part of our work begins. We have to implement test methods for every step. The methods we have created in the classCustomersPage.kt make this activity rather easy, especially with the features of the IDE.

Pay attention to steps related to the Then keyword. They should not contain any actions but should contain assertions.

Our class StepDefinitions can look as follows, but you can experiment here and implement these methods in your own way.

package eu.delimata.stepdefinitions

import eu.delimata.pages.CustomersPage
import io.cucumber.java.en.Given
import io.cucumber.java.en.Then
import io.cucumber.java.en.When
import org.assertj.core.api.Assertions

class StepDefinitions() {

private lateinit var customersPage: CustomersPage
private lateinit var searchSummaryAtVeryBeginning: String


@Given("the user is on the page")
fun theUserIsOnThePage() {
customersPage = CustomersPage()
customersPage.open()
searchSummaryAtVeryBeginning = customersPage.summaryText
}

@When("the user enters the value {string} in the text-input")
fun theUserEntersTheValueInTheTextInput(searchInput: String?) {
customersPage.setSearchInput(searchInput)
}

@When("the user selects value {string} in the drop-down")
fun theUserSelectsValueInTheDropDown(value: String?) {
customersPage.setSearchColumnDropDownListField(value)
}

@When("the user sets case sensitivity switch to {string}")
fun theUserSetsCaseSensitivitySwitchTo(isCaseSensitive: String?) {
val value = isCaseSensitive.toBoolean()
customersPage.setMatchCaseCheckboxField(value)
}

@When("the user clears filters")
fun theUserClearsFilters() {
customersPage.clickClearFiltersButton()
}

@Then("the user should see that search criteria are cleared")
fun theUserShouldSeeThatSearchCriteriaAreCleared() {
Assertions.assertThat(customersPage.searchInputText).isEmpty()
}

@Then("the user should see that the search result summary is as in the very beginning")
fun theUserShouldSeeThatTheSearchResultSummaryIsAsInTheVeryBeginning() {
Assertions
.assertThat(customersPage.summaryText)
.isEqualTo(searchSummaryAtVeryBeginning)
}
}
StepDefinitions.kt

Everything together we can run with the gradle test command.

Our results are stored in the directory allure-results. To visualize them we can execute:

allure serve allure-results

The results should open in the default web browser.

Allure results

REPOSITORY

If you are interested in details you can see the following repository:

The story was originally created by me, but it may contain parts that were created with AI assistance. My original text has been corrected and partially rephrased by Chat Generative Pre-trained Transformer to improve the language.

--

--