Пишем Selenium тесты с помощью kotlintest

zaz600
Kotlin Notes
Published in
7 min readSep 16, 2018

--

В этой заметке попробуем разобраться с тем, как писать Selenium тесты на языке программирования Kotlin при помощи тестового фреймворка kotlintest.

Какие технологии и библиотеки будем использовать::

Про kotlintest

Kotlintest — комплексный и гибкий инструмент для написания тестов на Kotlin, который позволяет писать тесты, используя совершенно разные стили, и включает из коробки множество матчеров и много других полезных вещей.

Пример теста:

class MyTests : StringSpec({
"length should return size of string" {
"hello".length shouldBe 5
}
"startsWith should test for a prefix" {
"world" should startWith("wor")
}
})

Полную документацию и другие примеры можно посмотреть тут.

Настройка проекта

Тесты будем писать в IntelliJ IDEA CE, но вы легко сможете использовать любую другую IDE.

  • Создаём новый проект: File — New Project — Gradle — Kotlin (Java):
  • Заполняем необходимые данные:
  • Настраиваем параметры Gradle: Use auto-import, Use default gradle wrapper:
  • Дожидаемся, когда gradle настроит проект и вносим изменения в build.gradle, чтобы подключить kotlintest:
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.2.61'
}

group 'io.github.zaz600'
version '1.0-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile 'io.kotlintest:kotlintest-runner-junit5:3.1.8'
}

test {
useJUnitPlatform()
}


compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
  • Создаём файл (File — New — Kotlin File/Class) TestHello.kt в каталоге src/test/kotlin/ со следующим содержимым:
import io.kotlintest.matchers.string.shouldStartWith
import io.kotlintest.shouldBe
import io.kotlintest.specs.StringSpec

class TestHello : StringSpec({
"length should return size of string" {
"hello".length.shouldBe(5)
}
"startsWith should test for a prefix" {
"world".shouldStartWith("wor")
}
}
)
  • Запускаем тесты через зелёный значок запуска слева от названия класса и убеждаемся, что тесты проходят.

Что будем тестировать

Тестировать будем валидацию формы регистрации на сервисе mail.ru. Адрес страницы https://account.mail.ru/signup

Валидация работает без отправки формы. Для того чтобы отобразилось сообщение с текстом ошибки, достаточно ввести недопустимое значение в одно поле и переключиться на любое другое.

Автоматизируем такие проверки для поля Желаемый почтовый адрес:

  • Невозможность задать значение меньше 4-х символов.
  • Невозможность задать значение больше 31 символа.
  • Невозможность вводить кириллицу.
  • Отображение ошибки при вводе недопустимых символов.

Подключаем Selenium к проекту

Добавляем Selenium в build.gradle. Тестировать будем в браузере Chrome, поэтому подключим и chrome-driver.

plugins {
id 'org.jetbrains.kotlin.jvm' version '1.2.61'
}

group 'io.github.zaz600'
version '1.0-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile 'io.kotlintest:kotlintest-runner-junit5:3.1.8'
testCompile group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '3.14.0'
testCompile group: 'org.seleniumhq.selenium', name: 'selenium-chrome-driver', version: '3.14.0'

}

test {
useJUnitPlatform()
}

compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}

Если у вас ещё не установлен chrome-driver, то самое время его установить, так как без него Selenium не сможет запустить Chrome и управлять им из тестов. В MacOS установку можно сделать через brew в терминале:

brew tap homebrew/cask
brew cask install chromedriver

Установка для других операционных систем описана тут.

Как вариант, можно воспользоваться плагином для Gradle, который скачает chrome-driver самостоятельно. Подробнее про плагин и как его подключить, можно почитать здесь.

Первая версия

Проверим, что всё установилось корректно. Для этого напишем тест, который запускает Chrome и переходит на страницу регистрации в mail.ru.

Создаём файл TestSignup.kt со следующим содержимым.

// TestSignup.ktimport io.kotlintest.matchers.string.shouldContain
import io.kotlintest.specs.StringSpec
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import java.util.concurrent.TimeUnit


class TestSignup : StringSpec() {
private val driver: WebDriver = ChromeDriver()
private val signupUrl = "https://account.mail.ru/signup"

init {
driver.manage()?.timeouts()?.implicitlyWait(10, TimeUnit.SECONDS)
driver.manage()?.window()?.maximize()

"Страница регистрации открывается" {
driver.run {
get(signupUrl)
pageSource.shouldContain("Регистрация")
quit()
}
}
}
}

Запускаем тест, проверяем, что на экране мелькает окно браузера и тест завершается успешно.

Вторая версия

Создадим класс SignupPage, в котором опишем веб-элементы страницы.

// SignupPage.ktimport org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.support.FindBy
import org.openqa.selenium.support.PageFactory
import org.openqa.selenium.support.ui.WebDriverWait

class SignupPage(private val driver: WebDriver) {

private val pageUrl = "https://account.mail.ru/signup"

init {
PageFactory.initElements(driver, this)
}

@FindBy(css = "input[data-blockid='email_name']")
lateinit var emailInput: WebElement

@FindBy(name = "password")
lateinit var password: WebElement

@FindBy(css = "div[class~='js-invalid_login_invalid_length']")
lateinit var invalidLoginLengthDiv: WebElement

@FindBy(css = "div[class~='js-invalid_login']")
lateinit var invalidLoginCharsDiv: WebElement

@FindBy(css = "div[class~='js-invalid_cyrillic']")
lateinit var invalidLoginCyrillicCharsDiv: WebElement

fun open() = driver.get(pageUrl)

fun verifyUrl() {
WebDriverWait(driver, 10).until { it.currentUrl == pageUrl }
}
}

В классе задаём подсказки для Selenium, как он может найти элементы на странице. Делаем это через аннотацию @FindBy, указывая какой селектор использовать и значение селектора.

Подробнее про CSS селекторы можно почитать тут, а про @FindBy и его параметры здесь.

Так как мы не хотим самостоятельно создавать объекты WebElement, то отдадим эту работу фабрике страниц Selenium. Вызов PageFactory.initElements создаст декоратор вокруг полей класса SignupPage и в момент обращения к этим полям, будет происходить поиск элементов в DOM. Подробнее об этом можно почитать в документации.

Модифицируем предыдущий тест, чтобы он использовал SignupPage.

import io.kotlintest.specs.StringSpec
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import java.util.concurrent.TimeUnit


class TestSignup : StringSpec() {
private val driver: WebDriver = ChromeDriver()
private val signupPage = SignupPage(driver)

init {
driver.manage()?.timeouts()?.implicitlyWait(10, TimeUnit.SECONDS)
driver.manage()?.window()?.maximize()

"Страница регистрации открывается" {
signupPage
.run {
open()
verifyUrl()
}
}
}
}

Чем в Kotlin отличаются друг от друга run, let, apply, also и т. п. можно почитать тут или тут.

Запускаем тест и проверяем, что он проходит.

Третья версия

Пишем тесты по тест-кейсам, про которые писали выше.

import io.kotlintest.data.forall
import io.kotlintest.shouldBe
import io.kotlintest.specs.StringSpec
import io.kotlintest.tables.row
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.support.ui.WebDriverWait
import java.util.concurrent.TimeUnit


class TestSignup : StringSpec() {
private val driver: WebDriver = ChromeDriver()
private val signupPage = SignupPage(driver)
private val wait = WebDriverWait(driver, 10)

init {
driver.manage()?.timeouts()?.implicitlyWait(10, TimeUnit.SECONDS)
driver.manage()?.window()?.maximize()

"Страница регистрации открывается" {
signupPage.run {
open()
verifyUrl()
}
}

"Желаемый почтовый адрес. Невозможность задать значение меньше 4-х символов" {
signupPage.run {
open()
emailInput.sendKeys("abc")
password.click()
wait.until { invalidLoginLengthDiv.isDisplayed }
}
}

"Желаемый почтовый адрес. Невозможность задать значение больше 31 символа" {
val str32 = "ee1439eb06e9457d86cbf1707a2937da"
val str31 = str32.substring(0, 31)
signupPage.run {
with(emailInput) {
clear()
sendKeys(str32)
}
password.click()
invalidLoginLengthDiv.isDisplayed.shouldBe(false)
emailInput.getAttribute("value").shouldBe(str31)
}
}

"Желаемый почтовый адрес. Невозможность вводить кириллицу" {
signupPage.apply {
emailInput.apply {
clear()
sendKeys("моймейл")
}
password.click()
wait.until { invalidLoginCyrillicCharsDiv.isDisplayed }
}
}

"Желаемый почтовый адрес. Отображение ошибки при вводе недопустимых символов" {
forall
(
row("qqq@12345"),
row("qqq@12_345"),
row("qqq#12345")
) { email ->
signupPage.run {
emailInput.apply {
clear()
sendKeys(email)
}
password.click()
wait.until { invalidLoginCharsDiv.isDisplayed }
}
}
}
}
}

Запускаем тест и проверяем, что все кейсы проходят успешно.

Если в кейсе Невозможность задать значение меньше 4-х символов поменять значение abc на abcd и снова запустить тест, то можно увидеть, как будет выглядеть падение теста. Он упадёт, поскольку сообщение об ошибке отображено не будет.

Версия четвёртая

К этому моменту у нас есть полноценные тесты, однако браузер по окончании всех тестов не закрывается. Давайте это исправим.

Сделать так, чтобы браузер автоматически закрывался по окончании тестов, можно двумя способами:

  • Реализовать функцию afterTest из интерфейса TestListener, в которой выполнить driver.quit(). Если вам понадобится выполнить какие-либо подготовительные действия перед тестом, то можно реализовать функцию beforeTest из этого же интерфейса.
  • Сделать браузер реализующим интерфейс Autocloseable и попросить kotlintest закрыть его.

Первый вариант выглядит так:

class TestSignup : StringSpec(), TestListener {
private val driver: WebDriver = ChromeDriver()
private val signupPage = SignupPage(driver)
private val wait = WebDriverWait(driver, 10)

override fun afterSpec(description: Description, spec: Spec) {
super<StringSpec>.afterSpec(description, spec)
driver.quit()
}


init {
...

Так как в нашем случае в конце тестов надо выполнить только одну команду, которая закроет браузер, мне больше нравится второй вариант.

Создаём файл WebDriverCloseable.kt со следующим содержимым:

// WebDriverCloseable.ktimport org.openqa.selenium.WebDriver
import java.io.Closeable

class WebDriverCloseable (private val delegate: WebDriver) : WebDriver by delegate, Closeable {
override fun close() {
delegate.quit()
}
}

Применяем новый класс-обёртку в тесте:

class TestSignup : StringSpec() {
private val driver: WebDriver = autoClose(WebDriverCloseable(ChromeDriver()))
private val signupPage = SignupPage(driver)
private val wait = WebDriverWait(driver, 10)


init {
...

Запускаем тест, проверяем, что он проходит и браузер закрывается автоматически.

Кстати, если запустить тесты через команды Gradle, то в конце можно будет увидеть стандартный отчёт о тестировании JUnit, который будет расположен в папке build/reports/tests/test/ в файле index.html

gradle clean test build

Исходный код можно посмотреть на github.

Заключение

Писать Selenium тесты с помощью kotlintest совсем несложно. Тесты получаются читаемыми (конечно, если они не трёхэкранные из-за сложной бизнес-логики).

Что можно улучшить?

  • Сделать базовый класс для тестов, чтобы не дублировать код создания вебдрайвера
  • Сделать базовый класс для страниц, куда спрятать общие для страниц функции, например, verifyUrl
  • Сделать класс, в котором поместить типичные действия, которые могут понадобиться многим тестам, например, функции авторизации на сайте.
  • Написать больше тестов.

Список литературы

--

--