Элементы функционального программирования появились в Java сравнительно недавно, но приобретает все большую популярность. Особенно в части stream API – наверное нет Java разработчика, который бы не слышал/читал/применял этот API для работы с коллекциями. К сожалению, большинство и не идет дальше использования Stream API, тогда как функциональный подход позволяет значительно упростить жизнь разработчикам автотестов. Ниже я расскажу про два примера такого упрощения – словари проверок и специализированные матчеры


Словари проверок.


Если Вы используете BDD подход, то наверняка, применяли параметризированные шаги проверки.


Когда нажимаем на кнопку «Кнопка»
Тогда проверить значения полей по БД
|Поле1|
|Поле2|
|Поле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.ru",e -> e.getAttribute("href").contains("qaconf.ru"));

Но зачем ограничивать себя только веб элементами? Сделаем статический 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’aх:


    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/

Поделиться с друзьями
-->

Комментарии (6)


  1. vladimir_dolzhenko
    20.05.2016 16:23

    Может стоит посмотреть в сторону Spock?


    1. vladimir_dolzhenko
      20.05.2016 16:32

      и еще одна презентация Spock vs JUnit, чтобы вдохновится и перестать бояться.


      1. qualife
        20.05.2016 21:11

        Я уже смотрел в сторону spock. Но, если честно, не вдохновляет. Если по существу, то, во-первых, одно другого не исключает, а скорее дополняет. А, во-вторых, spock — это груви, который далеко не всегда уместен.


  1. lany
    21.05.2016 11:17
    +1

    Всё же не надо позволять функциональному подходу съедать мозг полностью. Вот, например, это:


    public static BiFunction<String, Predicate<WebElement>, BaseMatcher<WebElement>> customMatcher = 
              (desc, pred) -> new BaseMatcher<T>() { ... };

    Функциональное программирование во все поля. Кто мешал написать обычный метод?


    public static BaseMatcher<WebElement> customMatcher(String desc, Predicate<WebElement> pred) {
        return new BaseMatcher<T>() { ... };
    }

    И вызывать не customMatcher.apply(desc, e -> blahblah), а просто customMatcher(desc, e -> blahblah)? А typedMatcher2 это просто должен быть метод от трёх аргументов (Class<T> cls, String desc, Predicate<WebElement> pred). Если по какой-то причине хочется класс зафиксировать (который вы, кстати, никак не используете), это делается с помощью карирования или частичного применения — стандартных ФП-паттернов. Зачем их изобретать вручную? Возьмите какой-нибудь javaslang и наслаждайтесь. Хотя мне здесь всё это кажется надуманной проблемой.


  1. Lanwen
    21.05.2016 11:41

    А вы в курсе что есть hamcrest, для которого можно просто обертку написать? И параметризация каким нить дата провайдером в JUnit? И junit5 с инжектами параметров, для того чтобы не нужно было делать странные конструкции по сопоставлению методов и названий?


  1. lany
    21.05.2016 14:09
    +2

    Я, кстати, в библиотеке StreamEx активно использую ФП при тестировании в связке с обычным JUnit. Например, у меня есть методы mapFirst() и mapLast() (преобразовать первый и последний элементы стрима, оставив остальное как есть). Тест может выглядеть так:


    streamEx(() -> StreamEx.of(0, 343, 999), 
              s -> assertEquals(asList(2, 343, 997), s.get().mapFirst(x -> x + 2).mapLast(x -> x - 2).toList()));

    Метод streamEx принимает сапплаер стрима (должен уметь выдавать идентичные стримы) и консюмер сапплаера стрима. Казалось бы, можно было написать просто:


    assertEquals(asList(2, 343, 997), StreamEx.of(0, 343, 999).get().mapFirst(x -> x + 2).mapLast(x -> x - 2).toList());

    Но так тест становится гораздо хуже. Эта штука streamEx за кадром скрывает много магии, вызывая консюмер в разных условиях. Во первых, естественно, стрим может быть параллельным или последовательным. Параллельный стрим может быть создан просто так, а ещё к нему могут быть конкатенированы пустые стримы слева или справа (это покрывает некоторые специфические частные случаи, про которые можно забыть). Ещё есть специальный режим, когда параллельный стрим модифицируется так, что промежуточные вызовы trySplit помимо нормальной работы иногда случайно отщепляют пустые куски в произвольных местах (по спецификации это не должно влиять на результат). Ещё один режим — стрим транслируется в сплитератор, а сплитератор назад в стрим (это добавляет обёрток, но не должно ничего сломать). По факту каждый такой однострочный тест — шесть тестов в разных условиях. И я могу при желании добавить новые (например, искусственно удалять некоторые характеристики, которые должны влиять только на быстродействие, но не на результат).


    А вот другая штука — тест сплитераторов:


    checkSpliterator("Distinct-3", Arrays.asList("b"), 
                      () -> new DistinctSpliterator<>(Arrays.asList("a", null, "b", "c", "b", null, "c", "b").spliterator(), 3));

    (DistinctSpliterator должен из входного сплитератора выбирать элементы, повторяющиеся не менее, чем заданное количество раз). Сплитератор (как и итератор) обходится только один раз, поэтому я подаю сапплайер сплитераторов. А эта штука checkSpliterator ничего не знает конкретно про DistinctSpliterator, но знает всё про контракт сплитератора и тестирует его во все поля. Обход через tryAdvance, forEachRemaining, частично tryAdvance, частично forEachRemaining, произвольное дерево сплитов с произвольным порядком дальнейшего обхода — всё должно дать одинаковый результат. Сапплаер по факту больше тысячи раз дёргается. С помощью этой штуки удавалось отловить очень хитрые баги.


    В общем, ФП действительно полезно в юнит-тестировании, как многие другие техники программирования.