Автотесты у функціональному стилі

Елементи функціонального програмування з'явилися в Java порівняно недавно, але набуває все більшу популярність. Особливо в частині stream API – напевно ні Java розробника, який би не чув/читав/застосовував цей API для роботи з колекціями. На жаль, більшість і не йде далі використання Stream API, тоді як функціональний підхід дозволяє значно спростити життя розробникам автотестів. Нижче я розповім про два приклади такого спрощення – словники перевірок та спеціалізовані матчеры
Словники перевірок.
Якщо Ви використовуєте BDD підхід, то напевно, застосовували параметризированные кроки перевірки.
Коли натискаємо на кнопку «Кнопка»
Тоді перевірити значення полів БД
|F1|
|F2|
|Поле3|

Для реалізації кроку такої перевірки з використанням ООП/процедурного підходу можна застосувати набір методів і switch для визначення імені поля для перевірки:
private void checkMinBalanceAmount(String checkMinAmount) throws Exception {
String minBalanceAmountStr = checkMinAmount;
String minBalanceAmount = String.format("%.2f", Double.parseDouble(minBalanceAmountStr));
String amountMinIF = amountLink.getText().replaceAll("(руб.|\\$|€)", "").replaceAll(" ", "");
Assert.assertEquals(minBalanceAmount, amountMinIF);
}

private void checkMaxBalanceAmount(String checkMaxAmount) throws Exception {
String maxBalanceAmountStr = checkMaxAmount;
String maxBalanceAmount = String.format("%.2f", Double.parseDouble(maxBalanceAmountStr));
String amountmaxIF = maxAmountDepositLink.getText().replaceAll("(руб.|\\$|€)", "").replaceAll(" ", "");
Assert.assertEquals(maxBalanceAmount, amountmaxIF);
}

private void checkBalanceAmount(String checkBalanceAmount) throws Exception {
String maxBalanceAmountStr = checkBalanceAmount;
String maxBalanceAmount = String.format("%.2f", Double.parseDouble(maxBalanceAmountStr));
String amountmaxIF = amountDepositLink.getText().replaceAll("(руб.|\\$|€)", "").replaceAll(" ", "");
Assert.assertEquals(maxBalanceAmount, amountmaxIF);
}

public void проверяет_значение_поля(String name) throws Throwable {
String query = "select * from deposit_and_account_data";
List<Map<String, String>> fetchAll = Db.fetchAll(query "main");
switch (name) {
case "Ім'я рахунки":
Assert.assertEquals(fetchAll.get(0).get("ACCOUNT_NAME"), nameDepositLink.getText());
break;
case "Дата закриття":
checkDate(fetchAll.get(0).get("CLOSE_DATE"));
break;
case "Код валюти":
checkCurrency(fetchAll.get(0).get("NAME"));
case "Сума незнижуваного залишку":
checkMinBalanceAmount(fetchAll.get(0).get("MIN_BALANCE_AMOUNT"));
break;
case "Максимальна сума для зняття":
checkMaxBalanceAmount(fetchAll.get(0).get("MAX_SUM_AMOUNT"));
break;
case "Сума вкладу":
checkBalanceAmount(fetchAll.get(0).get("BALANCE_AMOUNT"));
break;

default:
throw new AutotestError("Несподіване поле");
}

У коді вище немає нічого поганого, він добре структурований. Але у нього є проблема – трудомістка додавання ще однієї перевірки: потрібно, по-перше, реалізувати перевірку, а, по-друге, додати її в світч. Другий крок видається надмірним. Якщо застосувати «словник перевірок», то можна обійтися тільки першим кроків.
Словник перевірок – це Map, в якій ключем є ім'я перевірки, а значенням – функція, що приймає в якості параметрів запис з БД, а повертає Boolean. Тобто java.util.function.Predicate
Map<String,Predicate<Map<String,String>>> checkMap = new HashMap<>();

Переписуємо перевірки:
checkMap.put("Ім'я рахунки",exp -> exp.get("ACCOUNT_NAME").equals(nameDepositLink.getText()));

Переписуємо метод виклику перевірок:
public void проверяет_значение_поля(String name) throws Throwable {
String query = "select * from deposit_and_account_data";
Map<String, String> expected = Db.fetchAll(query "main").get(0);
Assert.assertTrue(name,
Optional.ofNullable(checkMap.get(name))
.orElseThrow(()->new AutotestError("Несподіване поле"))
.test(expected));
}

Що відбувається у фрагменті вище: Намагаємося отримати перевірку з іменем поля Optional.ofNullable(checkMap.get(name)), якщо вона NULL, викидаємо виняток. Інакше виконуємо отриману перевірку.
Тепер для того, щоб додати нову перевірку її досить додати в словник. У методі виклику перевірок вона стає доступна автоматично. Повний вихідний код прикладу і інші приклади використання ФП для автотестів доступні в репозиторії на GitHub:
https://github.com/kneradovsky/java8fp_samples/
Custom Matchers
Практика показує, що ассерти досить рідко застосовуються в автотестах Selenuim WebDriver. На мій погляд, найбільш ймовірною причиною цього є те, що стандартні Matchers не надають функціональності для перевірки стану WebElement. Чому потрібно застосовувати assertions? Це стандартний механізм, який підтримується будь-якими засобами генерації звітів та представлення результатів тестування. Навіщо винаходити велосипед, якщо можна його доопрацювати.
Як функціональний підхід може зробити використання assertions для перевірки властивостей і стан WebElement'ів зручним? І навіщо обмежуватися тільки веб елементами?
Уявімо, що у нас є функція, яка приймає 2 аргументу: повідомлення про помилку: у разі провалу і функцію-предикат (приймає перевіряється WebElement і повертає результат перевірки), а повертає Matcher.
public static BiFunction<String, Predicate<WebElement>, BaseMatcher<WebElement>> customMatcher = 
(desc, pred) -> new BaseMatcher<T>() {
@Override
public boolean matches(Object o) {
return pred.test((WebElement) o);
}

@Override
public void describeTo(Description description) {
description.appendText(desc);
}
};
}

Це дозволяє конструювати будь-які перевірки зі спеціалізованими повідомленнями про помилку.
BaseMacther<WebElement> m1 = customMatcher.apply("Результати повинні містити qaconf.ua",e -> e.getAttribute("href").contains("qaconf.ua"));

Але навіщо обмежувати себе лише веб елементами? Зробимо статичний generic метод, який прийме тип об'єкта, поверне функцію від 2 аргументів: повідомлення про помилку: у разі провалу і функцію-предикат (приймає об'єкт заданого тип і повертає результат перевірки) і повертає Matcher.
public static <T> BiFunction<String, Predicate<T>, BaseMatcher<T>> typedMatcher2(Class<T> cls) {
return (desc, pred) -> new BaseMatcher<T>() {
@Override
public boolean matches(Object o) {
return pred.test((T) o);
}

@Override
public void describeTo(Description description) {
description.appendText(desc);
}
};
}

Тепер у нас є спеціалізований матчер, який можна застосовувати в assert'ах:
BiFunction<String, Function<Predicate<WebElement>, BaseMatcher<WebElement>>> webElMatcherSupp = typedMatcher2(WebElement.class);

BaseMatcher<WebElement> shouldBeTable = apply("Should be Table",e->e.getTagName().equalsIgnoreCase("table"));

assertThat(elem2Test,shouldBeTable);

В комбінаціях з іншими матчерами:
assertThat(elem2test,not(shouldBeTable));

так
BaseMatcher<WebElement> hasText1 = webElMatcherSupp.apply("Should be contain text1",e->e.getText().equalsIgnoreCase("text1"));

assertThat(elem2test,allOf(not(shouldBeTable),hasText1));

Крім цього матчеры можна використовувати в припущеннях (assumptions)
assumeThat(elem2test,not(shouldBeTable));

Але і це ще не все. Можна створити параметризований спеціалізований матчер:
Function<String,BaseMatcher<WebElement> textEquals = str -> webElMatcherSupp.apply("Text should equals to: " + str,e-> e.getText().equals(str));
assertThat(elem2test,textEquals.apply("text2"));
assertThat(elem2test,not(textEquals.apply("text3")));

Таким чином отримуємо спеціалізований матчер, у якого повідомлення і перевірка параметризуется будь-яким значенням, переданим на етапі виконання.
Висновок:
Застосування функціонального підходу в розробці автотестів дозволяє скоротити кількість коду, з іншого – підвищити його читабельність. Власні матчеры дозволяють легко створювати набори типізованих перевірок, доповнюючи стандартний механізм assertions. Словники перевірок позбавляють від необхідності виконання непотрібної роботи.
Повний код прикладів знаходиться в репозиторії: https://github.com/kneradovsky/java8fp_samples/
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.