Kotlin для автотестов: боевой опыт
Привет, друг! Меня часто просят рассказать о нашей практике написания тестов на Котлине. Наконец-то я нашел время и решил поделиться своим опытом.
Начну с того, что еще в сентябре 2017 года я рассказывал о Котлине на конференции QAFest. С того времени утекло много воды, кое-что я переосмыслил.
Давай сначала поясню мотивацию писать на Котлине. Джава подутомила. Реально, когда ты пишешь тесты, то некоторые конструкции хотелось бы опустить. Для примера покажу типичный PageObject в моих проектах:
@PageUrl("/")
class LoginPage {
SelenideElement userNameInput = $("#userName");
SelenideElement passwordInput = $("#password");
SelenideElement signInBtn = $("#loginBtn")
public SelenideElement errorMessage = s("#page__loginByEmail > div:nth-child(3) > div")
public logisAs(User user){
userNameInput.setValue(user.getName());
passwordInput.setValue(user.getPassword());
signInBtn.click();
}
}
Простой класс с полями и методами. Можно холиварить, хорошо так писать или плохо, но я так пишу везде. Для целостности картины покажу тест:
class LoginTest {
@Test
public void testCanNotLoginWithWrongCredentials(){
User user = TestData.getUser();
LoginPage loginPage = open(LoginPage.class);
loginPage.loginAs(user);
loginPage.errorMessage.shouldHave(text("Bad credentials"));
}
}
Все достаточто просто и без лишнего. Чтобы упростить написание тестов и убрать некоторые излишества, я пробовал использовать Lombok. С ним, вроде как, все хорошо, кроме плагина для Idea. В общем, использовать можно, но будь готов к сюрпризам.
В целом Котлин еще был выбран из-за желания попробовать его в реальном проекте. Хайпа вокруг языка достаточно, да и опыт проб в домашних проектах показывал, что все будет хорошо.
Для проекта я выбрал Kotlin + Selenide + Allure + Gradle. Имхо сейчас все инструменты, окромя Котлина, стандарт для Джава проектов.
Костыль первый!
Знак $ является зарезервированным в Котлине, поэтому пришлось написать две обертки:
fun s(locator: String): SelenideElement {
return Selenide.`$`(locator)
}
fun ss(locator: String): ElementsCollection {
return Selenide.`$$`(locator)
}
Теперь, значит, можно переписать наш PageObject на Котлине:
class LoginPage : Page() {
override val url: String = "/"
val userNameInput = s("#username")
val passwordInput = s("#password")
val sighInBtn = s("#loginBtn")
val errorMessage = s("#page__loginByEmail > div:nth-child(3) > div")
fun loginAs(user: User): MainPage {
userNameInput.value = user.name
passwordInput.value = user.password
sighInBtn.click()
return MainPage()
}
}
Тест будет выглядеть так:
class LoginTest {
@Test
fun testCanNotLoginWithWrongCredentials() {
val user = TestData.getUser()
val loginPage = open(::LoginPage)
loginPage.loginAs(user)
loginPage.errorMessage.shouldHave(text("Bad credentials"))
}
}
Ты можешь возразить, что ничего особенно не поменялось и будешь абсолютно прав. Для написания Web тестов особого профита нет. Да, код становится писать чуть-чуть проще, используя val.
Что неудобно с переходом на Котлин - нужно много делать static import. Особенно в случаях с Conditions.text(). Пока что Idea не позволяет импортить это на лету, как в Джаве.
Эту штуку в принципе можно легко поправить, добавив BDD style ассерты для Selenide.
Пишем метод расширения и реализацию ассертов:
val SelenideElement.should:ExpextElement get() {
return ExpextElement(this)
}
class ExpextElement(private val actual: SelenideElement){
val have: Have = Have()
val be: Be = Be()
inner class Have{
fun text(text:String){
actual.shouldHave(Condition.text(text))
}
fun exactText(text: String?) {
actual.shouldHave(Condition.exactText(text))
}
}
inner class Be{
val visible:Unit get() {
actual.shouldBe(Condition.visible)
}
}
}
Теперь проверки в Selenide можно писать как старым методом:
loginPage.siteLogo.shouldBe(visible)
loginPage.errorMessage.shouldHave(text("Bad credentials"))
…так и более Котлин ориентированным:
loginPage.siteLogo.should.be.visible
loginPage.errorMessage.should.have.text("Bad credentials")
Мне такой варинт нравится по нескольким причинам:
-
не нужно постоянно делать static import;
-
работает автокомлит в Idea;
-
коллегам, которые слабо знают Selenide, не нужно объяснять разницу между should, shouldBe и shouldHave. Я встречал кейсы, где люди пишут
element.shouldHave(blank)
.
Так, с Web тестами вроде как понятно. Еще покажу пример использования для работы с базой. Я уже писал подобную заметку, но тогда это были первые шаги, теперь же - как ретроспективка.
Значит, нормальной ORM я для Котлина не нашел. Пробовал и Exposed, и другие, которые можно найти на Github. Некоторые не поддерживают MS SQL Server, некоторые обладают каким-то упоротым API.
Короче говоря, пришлось писать свой велосипед. За основу я взял Apache DBUtils.
fun QueryRunner.query(sql: String): List<Map<String, Any?>> {
val resultSetHandler = ResultSetHandler<List<Map<String, Any?>>> { rs ->
val meta = rs.metaData
val cols = meta.columnCount
val result = arrayListOf<Map<String,Any?>>()
while (rs.next()) {
val map = mutableMapOf<String, Any?>()
for (i in 0 until cols) {
val columnName = meta.getColumnName(i + 1)
map[columnName] = rs.getObject(i + 1)
}
result.add(map)
}
result
}
return query(sql,resultSetHandler)
}
inline fun <reified T> QueryRunner.findOne(sql: String): T {
return BeanHandler(T::class.java).run { query(sql, this) }
}
inline fun <reified T> QueryRunner.findAll(sql: String): MutableList<T> {
return BeanListHandler(T::class.java).run { query(sql, this) }
}
Создадим еще классы таблиц как пример:
data class Suppliers(var id: String? = null,
var company: String? = null,
var currency: String? = null)
Теперь можно работать с базой:
fun selectAllSuppliers(): MutableList<Suppliers> {
val sql = """
SELECT *
FROM Suppliers;
"""
return queryRunner.findAll(sql)
}
Добавив библиотеку Expekt, тесты можно писать так:
class TestDB {
val db = Database()
@Test
fun testCanGetAllSuppliers(){
db.selectAllSuppliers().should.have.size(3)
}
}
В этом аспекте все значительно проще. Мне понадобилось добавить пару Extension методов для класса QueryRunner и прикрутить готовую библиотеку для удобных ассертов.
Вывод: пока что впечатления о самом языке Котлин положительные. Интеграция с суровыми Java библиотеками иногда может вызвать панику. Пару раз у нас Котлин не желал компилироваться и падал со странными ошибками о том, что Gradle daemon умер. Оказалось, ему просто не хватало Heap памяти. По факту я нашел в баг трекере тикет на эту проблему и, вроде бы, починилось оно переходом на самую свежую версию Котлина и Грейдла. Сейчас такого не наблюдается. Тьфу-тьфу.
Как видишь, большого преимущества перехода с Джавы на Котлин не наблюдается. Некоторые вещи становится делать удобнее, но не намного. Буду ли я пробовать делать еще проекты на Котлине? Пока не могу ответить - все упирается в рынок труда. Найти хороших автоматизаторов, которые могут делать работу хорошо на Джаве, - сложно. Тех, кто хотя бы как-то видел Котлин, среди них еще меньше.
В целом я продолжаю следить за этим языком. Было бы полезно узнать опыт других ребят, которые пробовали что-то делать на Котлине. Если у тебя такой опыт есть, пиши в комментарии или в личку. Подписывайся на телеграмм канал, чтобы получать самые свежие мысли и соображения на тему автоматизации тестирования.