Эффективные UI-тесты на Selenide

    В ожидании чудес


    Канун Нового Года — время чудес. В преддверии нового года мы все вспоминаем год уходящий и строим планы на следующий. И надеемся, что все проблемы останутся в прошлом, а в новом году случится чудо, и мы заживём по-новому.

    Какой же Java разработчик не мечтает о чуде, которое осенит его и позволит стать Самым Крутым На Свете Java Программистом.

    Хорошие новости: я хочу рассказать как раз о таком чуде.

    Имя ему — автоматические тесты!


    Фу, тесты?


    Да. Настоящим мастером своего дела вас сделают не чудо-фреймворки, не микро/пико/нано сервисы, а дисциплина. Дисциплина, которая говорит, что программист может считать дело законченным не тогда, когда код готов, а тогда, когда к нему написаны и запущены автоматические тесты. И если с юнит-тестами всё более-менее ясно, то UI-тесты пока остаются для разработчиков тёмным лесом.

    Да ну, это же нудно?


    О нет, поверьте мне! Написание грамотных автотестов — это хороший вызов, тут есть над чем пораскинуть мозгами. И это может быть очень весело и интересно. Только надо использовать правильные инструменты.

    Правильный инструмент для написания UI-тестов — это:

    Selenide


    Selenide — это библиотека для написания лаконичных и стабильных UI тестов с открытым исходным кодом.

    Selenide — идеальный выбор для разработчиков, потому что у неё очень низкая кривая обучения. Вам не придётся заморачиваться со всеми этими техническими подробностям, на которые обычно тестировщики-автоматизаторы тратят так много времени: нюансы работы с браузерами, типичные проблемы с таймингом и аяксом.

    Посмотрим, как выглядит простенький тест на Selenide:

    public class GoogleTest {
      @Test
      public void user_can_search_everything_in_google() {
        open("http://google.com/ncr");
        $(By.name("q")).val("selenide").pressEnter();
    
        $$("#ires .g").shouldHave(size(10));
    
        $("#ires .g").shouldBe(visible).shouldHave(
            text("Selenide: concise UI tests in Java"),
            text("selenide.org"));
      }
    }
    


    (естественно, вместо Google здесь будет ваше веб-приложение)

    Что здесь происходит?

    • Вы открываете браузер всего-навсего одной командой open(url)
    • Вы ищете элемент на странице командой $.
      Вы можете найти элемент по имени, ID, CSS селектору, атрибуту, xpath и даже по тексту.
    • Вы совершаете некие действия с элементом: в данном случае вводите текст командой val() и нажимаете ввод с помощью команды pressEnter().
    • Вы проверяете результат: ищете все результаты поиска с помощью $$ (она возвращает коллекцию элементов). Вы проверяете размер и содержимое коллекции.


    Этот тест легко читается, не правда ли?
    Этот тест легко пишется, не правда ли?

    А главное, этот тест легко запускается. Убедитесь сами:


    Погружаемся глубже


    Конечно, в жизни не всё так просто. Написание автотестов подразумевает кучу проблем, ведь не зря разработчики так их боятся — больше, чем любого наисложнейшего фреймворка или технологии.

    Но и здесь Selenide облегчает нашу жизнь, решая большинство этих проблем из коробки.

    Давайте рассмотрим типичные проблемы UI-тестов подробнее.

    Проблемы с аяксом и таймаутами

    В наше время веб-приложения все сплошь динамические. Каждый кусочек приложения может быть нарисован/изменён динамически в любой момент времени. Это создаёт проблемы для автоматических тестов. Тест, который ещё вчера был зелёным, может внезапно стать красным безо всяких изменений в коде. Просто потому, что браузер сегодня встал не с той ноги и запустил вон тот javascript чуточку медленнее, и тест успел кликнуть кнопочку раньше, чем она до конца отрисовалась.

    Это прям вечная проблема у всех. Поэтому автоматизаторы пихают везде «слипы».

    Тем более удивительно, насколько простым и надёжным способом Selenide решает эту проблему.

    Если коротко, в Selenide каждый метод умеет немножко подождать, если надо. Люди называют это «умными ожиданиями».

    Когда вы пишете
    $("#menu").shouldHave(text("Hello"));
    

    Selenide проверит, существует ли элемент с ID=«menu». И если нет, Selenide чуть-чуть подождёт, проверит ещё. Потом ещё подождёт. И только когда элемент появится, Selenide проверит, что у него нужный текст.

    Конечно, нельзя ждать вечно. Поэтому Selenide ждёт не больше 4 секунд. Естественно, этот таймаут можно настраивать.

    Не сделает ли это мои тесты медленными?

    Нет, не сделает. Selenide ждёт, только если надо. Если элемент изначально присутствует на странице — Selenide не ждёт. Если элемент появился через 300 мс — Selenide ждёт только 300 мс. Это именно то, что вам нужно.

    Множество встроенных проверок

    А что ещё вы можете проверять на странице, помимо текста? Довольно много всего.

    Например, вы можете проверить, что элемент видимый (visible). Если пока нет, Selenide подождёт до 4 секунд.
    $(".loading_progress").shouldBe(visible);
    


    Вы можете даже проверить, что элемент не существует. Если элемент всё же найден, Selenide предположит, что он вот-вот пропадёт и подождёт до 4 секунд.
    $(By.name("gender")).should(disappear);
    


    Вы можете делать несколько проверок в одной строке (т.н. «fluent API» и «method chain»), что сделает ваши тесты ещё более лаконичными:
    $("#menu")
      .shouldHave(text("Hello"), text("John!"))
      .shouldBe(enabled, selected);
    


    Коллекции

    Selenide позволяет вам очень удобно работать с коллекциями элементов. Вы можете проверять сразу множество элементов в одной строке.

    Например, вы можете проверить, что на странице ровно N таких-то элементов:
    $$(".error").shouldHave(size(3));
    


    Вы можете отфильтровать подмножество элементов:
    $$("#employees tbody tr")
      .filter(visible)
      .shouldHave(size(4));
    


    Вы можете проверить тексты элементов. В большинстве случаев этого достаточно, чтобы проверить целую таблицу или строку в таблице:
    $$("#employees tbody tr").shouldHave(
      texts(
          "John Belushi",
          "Bruce Willis",
          "John Malkovich"
      )
    );
    


    Скачивание/закачивание файлов

    С Selenide закачивать файлы предельно просто:
    $("#cv").uploadFile(new File("cv.doc"));
    


    Вы даже можете закачать несколько файлов разом:
    $("#cv").uploadFile(
      new File("cv1.doc"),
      new File("cv2.doc"),
      new File("cv3.doc")
    );
    


    И скачивание файлов тоже крайне просто:
    File pdf = $(".btn#cv").download();
    


    Тестирование «динамичных» веб-приложений

    Некоторые веб-фреймворки (такие как GWT) генерируют совершенно нечитаемый HTML, не поддающийся анализу. Там нет постоянных ID, имён или классов.

    Это прям вечная проблема у всех. Поэтому автоматизаторы пихают везде длиннющие «xpath» и вынуждены их поддерживать до конца жизни.

    Чтобы решить эту проблему, Selenide предлагает искать элементы по тексту.

    import static com.codeborne.selenide.Selectors.*;
    
    $(byText("Привет, хабр!"))             // находит элемент по тексту целиком
       .shouldBe(visible);
    
    $(withText("хаб"))                     // находит элемент по подстроке
       .shouldHave(text("Привет, хабр!"));
    


    Вопреки распространённому мнению, поиск элементов по тексту — не такая уж плохая идея. Между прочим, именно так ищет элементы реальный пользователь. Он не ищет элементы по ID или классу, и уж тем более не по XPATH. Он ищет по тексту. (ну, ещё по цвету, но это труднее поддаётся автоматизации).

    Ещё в Selenide есть несколько полезных методов для поиска дочерних или родительских элементов. Это позволяет вам навигировать между элементами без опознавательных знаков.

    $("td").parent()
    $("td").closest("tr")
    $(".btn").closest(".modal")
    $("div").find(By.name("q"))
    


    Например, вы можете найти ячейку в таблице по тексту, затем найти содержащую её строку tr и найти в этой строке кнопку «Save»:
    $("table#employees")
      .find(byText("Joshua"))
      .closest("tr.employee")
      .find(byValue("Save"))
      .click();
    


    Page Object


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

    Приведённый выше пример гугла можно переделать на page object таким образом:

      @Test
      public void userCanSearch() {
        GooglePage page = open("http://google.com/ncr", GooglePage.class);
        SearchResultsPage results = page.searchFor("selenide");
        results.getResults().shouldHave(size(10));
        results.getResult(0).shouldHave(text("Selenide: concise UI tests in Java"));
      }
    

    Page Object для страницы поиска гугл:

    public class GooglePage {
      public SearchResultsPage searchFor(String text) {
        $(By.name("q")).val(text).pressEnter();
        return page(SearchResultsPage.class);
      }
    }
    

    И для страницы результатов поиска:

    public class SearchResultsPage {
      public ElementsCollection getResults() {
        return $$("#ires .g");
      }
      public SelenideElement getResult(int index) {
        return $("#ires .g", index);
      }
    }
    


    Но не злоупотребляйте пэдж объектами.
    Хочу обратить ваше внимание, что UI-тестов должно быть мало. По той простой причине, что это всё-таки браузер,
    аякс, javascript, а всё это сравнительно медленно и нестабильно. Напишите один-два UI-теста, которые проверят, что
    приложение в целом работает: страничка открывается, текст отрисовывается, кнопки нажимаются, JavaScript не грохается.

    А всевозможные комбинации и редкие случаи обязательно проверяйте с помощью модульных тестов.

    Типичная ошибка — проверять всё через UI. Этим особенно страдают тестировщики в тех компаниях, где разработчики не пишут
    модульных тестов. Бедным тестировщикам просто ничего не остаётся, кроме как городить огромную неповоротливую кучу медленных
    и нестабильных UI-тестов и впрягаться в их пожизненную поддержку.

    Но ты ж программист! Не заставляй людей мучаться.


    … и много других полезняшек

    В Selenide есть ещё много функций, таких как:

    $("div").scrollTo();
    $("div").innerText();
    $("div").innerHtml();
    $("div").exists();
    $("select").isImage();
    $("select").getSelectedText();
    $("select").getSelectedValue();
    $("div").doubleClick();
    $("div").contextClick();
    $("div").hover();
    $("div").dragAndDrop()
    zoom(2.5);
    и т.д.
    


    Хорошая новость в том, что вам не нужно всё это запоминать. Просто наберите $, точку и начните писать примерно, что вы хотите. Например «val» или «enter». И посмотрите, какие варианты предложит ваша IDE.

    Используйте мощь IDE! Не засоряйте голову деталями и сконцентрируйтесь на бизнес-логике.

    image

    Сделаем мир лучше


    У верю, что мир станет лучше, когда все разработчики будут писать автоматические тесты для своего кода. Когда разработчики будут спокойно вставать в 17:00 и идти к своим детям, не боясь, что они что-то сломали своими изменениями.

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

    Давайте сделаем мир лучше с помощью автоматических тестов! Будьте уверены в своём софте, и вам не придётся скрещивать пальцы на удачу перед каждым релизом.

    selenide-logo
    С Новым Годом!
    ru.selenide.org
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 12
    • +1
      Обзор хороший, но первый пример теста, ИМХО, ужасны. Если тесты писать так — получится гигантская лапша в каждом тесте. Потому что если нам нужно протестировать функциональность гугла — мы в каждом етсте будем вынуждены открывать его страницу, что-то делать, проверять, опять делать… Нужен какой-то BDD типа Cucumber.

      Кроме того очень не хватает обзора PageObject'ов, которые с Selenide делать очень просто.
      • 0
        Ох, плохие практики настолько прочно вошли в нашу жизнь, что уже кажутся хорошими. :(

        Интерфейс (т.е. UI) поиска гугла, как известно, крайне простой. Для тестирования UI больше тестов не нужно.
        Вот этого одного теста достаточно.

        BDD вообще-то нужно для того, чтобы с заказчиком проговаривать бизнес-требования и записывать в виде тестов. В данном случае требований немного и они все в тесте уже отражены. Вы, видимо, имеете в виду, что понадобится много тестов для тестирования нюансов поиска — но это нужно тестировать не через UI. Там обязательно надо использовать модульные и интеграционные тесты, а вовсе не BDD и не Cucumber.

        А про PageObject надо добавить, спаисбо.
        • 0
          Хм, я бы поспорил. На самом деле если вы тестируете гугл — вы ещё будете наверное несколько вещей проверять. Например что по ходу ввода вываливаются подсказки. Сколько блоков AdWords есть на странице. Что при поиске знаменитостей вываливается карточка с краткой информацией. Аналогично при поиске георафических единиц. Что до того как мы начали искать мы видим две кнопки для поиска. Что результаты поиска отличаются когда мы залогинены и когда нет (ладно, это спорно, согласен). Это всё нюансы интерфейса.

          А ещё BDD хорош тем, что нам самим из текстов понятно что имненно мы тестируем. Самодокументирующийся код и вот это вот всё.

          • 0
            Согласен, функциональности там действительно значительно больше, чем мне казалось. Наверное, я никогда не искал знаменитостей. :)
            Но изначальное утверждение, что первый тест ужасен — категорически не могу принять. Я могу написать качественные тесты на все перечисленные функции безо всякого BDD.

            > А ещё BDD хорош тем, что нам самим из текстов понятно что имненно мы тестируем. Самодокументирующийся код и вот это вот всё.

            А кто же вам мешает любые другие тесты писать так, чтобы было понятно?
            А кто вам мешает любой код делать самодокументирующимся?
            Почему для этого непременно нужен какой-то там магический BDD? Без BDD никак, а возьми BDD — и оно само вдруг получится? Ага.
            • 0
              Он сделает написание самодокументирующегося кода проще, а код структурированнее. Более того, BDD-фреймворки дают возмоность простыми словами, насколько угодно длинно описать каждый сценарий, юезрстори и степ. В джаве, увы, у вас не может быть, например, пробелов в названии метода. И это автоматически значит что вам придётся эти названия методов расшифровывать.

              Насчёт того, что вы можете написать качественные тесты — я, в общем-то, не сомневаюсь. Вопрос в том, сколько будет стоить поддержка этого дела. Так-то вообще все тесты чего угодно можно загнать в один метод и сказать, что так всё просто и понятно.

              Но конечно же всё что я написал можно сделать и без BDD, просто сложнее. Можно Allure приделать, например, который даст методам нормальные дескрипшны, к TestNG который даёт зависимости методов друг от друга.
              • 0
                Для меня ваш cucumber — это огромная пушка, цель которой предоставить аналитикам самим писать тесты.
                Это дополнительный уровень инфраструктуры в вашем коде. Имхо, проще добавить groovy/scala в проект в тесты и наслаждаться: etorreborre.github.io/specs2

                Более того, cucumber тесты тупо дольше писать — надо писать именно те фразы, которые были определены для конкретного действия: github.com/cucumber/cucumber-jvm/blob/master/examples/java-calculator/src/test/java/cucumber/examples/java/calculator/RpnCalculatorStepdefs.java — ну почему я должен написать 'I add 1 and 2' и реализацию метода вместо простого calc.push(1).add(2) (предположение как выглядел бы вменяемый интерфейс к калькулятору)? Вместо одной строчки!

                Это Вам кажется, что сложнее, потому что Вы не пробовали. Попробуете писать на обычных тест-фреймворках — у вас код будет лаконичнее и тестов, и самого тестируемого кода.
                • 0
                  Извините, я не буду дальше спорить — мы как-то ни о чём спорим. Я пробовал тестировать по всякому. Я знаю людей, которые считают что Selenide — наколеночное поделие по сравнению с Serenity. Сам я начинал когда-то с JUnit + Selenium. И до Cucumber + Selenide я дошёл эволюционным путём.
            • 0
              > На самом деле если вы тестируете гугл — вы ещё будете наверное несколько вещей проверять.
              Только это будет уже другие тесты.

              > А ещё BDD хорош тем, что нам самим из текстов понятно что имненно мы тестируем. Самодокументирующийся код и вот это вот всё.
              Фигня это все. Этот недоанглийский подходит только для очень простых вещей, которые легко читаются и без него. Правильное использование PageObject'ов с легкостью заменяет весь этот модный BDD.
              • 0
                Я говорил про userstory. Их в обычный тест не включишь без сециальных инструментов. Хоть на русском, хоть на английском, хоть на любом другом.
        • 0
          Если коротко, в Selenide каждый метод умеет немножко подождать, если надо. Люди называют это «умными ожиданиями».

          Правильно я понимаю, что это должно полностью избавить от проблем со stale element reference exception? И каким образом реализовано это умное ожидание?
          • 0
            Да, это должно избавить от проблем со stale element exception в большинстве случаев. Совсем полностью невозможно даже теоретически. Всегда можно написать приложение и тест, который выкинет stale element, как ни старайся.
            • 0
              А реализовано «умное ожидание» очень просто:

              while (прошло меньше 4 секунд) {
                try {
                  element.doOperation();
                }
                catch (WebDriverException e) {
                  sleep(10);
                }
              }
              

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.