Cucumber in Kotlin
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”.
After successfully setting up the project, you should see the following 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)
}
}
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.
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.