Пишем Selenium тесты с помощью kotlintest
В этой заметке попробуем разобраться с тем, как писать Selenium тесты на языке программирования Kotlin при помощи тестового фреймворка kotlintest.
Какие технологии и библиотеки будем использовать::
- Kotlin
- kotlintest
- gradle
- Selenium
- PageObject
Про 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
- Сделать класс, в котором поместить типичные действия, которые могут понадобиться многим тестам, например, функции авторизации на сайте.
- Написать больше тестов.
Список литературы
- Kotlin
- kotlintest
- E2E testing with Selenium and Kotlin
- CSS3 селекторы
- Mastering Kotlin standard functions: run, with, let, also and apply
- The difference between Kotlin’s functions: ‘let’, ‘apply’, ‘with’, ‘run’ and ‘also’
- The PageFactory
- Selenium
- Использование паттерна Page Object
- chromedriver
- Исходный код примеров