Selenide: Лаконичные UI тесты на Java

Всем привет!

Я хочу рассказать про библиотеку для написания UI тестов Selenide.

Selenide - это обёртка вокруг Selenium WebDriver, дающая удобный синтаксис и решающая большинство проблем, связанных с Ajax и таймаутами.

 

Сразу пример кода:

 

@Test
public void userCanLoginByUsername() {
  open("http://google.com");
  $(By.name("q")).setValue("selenide").pressEnter();
  $("#results").shouldHave(text("Selenide.org")); // Само подождёт, пока у элемента появится нужный текст
}

Как видите, код теста простой и читаемый. Selenide избавляет вас от необходимости открывать/закрывать браузер и пропихивать во все методы переменную webdriver.

Заинтересовавшихся добро пожаловать в вводную статью "Что такое Selenide". А в этой статье есть много примеров для сравнения, как сделать одну и ту же вещь с помощью голого WebDriver и Selenide. А ещё про Selenide рассказывали на недавней конференции SeleniumCamp 2013 в Киеве. Видео есть здесь.

 

В одну строчку!

Selenide в одну строчку решает многие проблемы, которые с Selenium WebDriver требуют значительно больше кода. 

Например, "как снимать скриншоты" - с Selenide решается в одну строку:

 

 @Rule
   public ScreenShooter makeScreenshotOnFailure = ScreenShooter.failedTests();

 

 

Другой пример - как подождать, пока элемент появится на странице после Ajax запроса.

С Selenide над этим вообще не надо ломать голову - он сам подождёт всего чего нужно:

 

$("#results").shouldHave(text("Selenide.org")); // Само подождёт, пока у элемента появится нужный текст

 

Совсем свежий пример: как проверить, существует ли элемент.

Опять же, с Selenide не нужны try/catch и вся эта монструзятина. В одну строчку:

 

  if ($("#results").exists()) {  ... существует ...}
 

 

И так далее. 

 

Кто попробует - расскажите, как прошло. Что получилось, что не получилось, что понравилось, что не понравилось.

 

 

 

2 лайка

IMHO - все эти прокси, рефлекшены, делегаты навевают грусть и тоску. Ведь у автоматизаторов есть более важные задачи, чем изучение дезайн паттернов. В ведь если использовать этот фреймворк - в любом случае придется лезть в их дебри.

Простой пример - в приложении есть отдельные элементы,в которых стандартные isDisplayed, isEnabled и т.д. не работают - как в этом случае решить проблему в рамках Selenide?

Что такое "отдельные элементы" и что значит "не реботает метод "isDisplayed"?

Как он может не работать? Элемент либо виден, либо не виден - другого не дано. 

элементарно не работает из-за багов селениума или специфики реализации. Например чекбокс представлен не просто как input, а как span в котором image - в этом случае стандартный isSelected не проходит. И т.д. и т.п.

Надо живые примеры например с isDisplayed - могу показать ;-)

Ну так о чём разговор...

Если у вас кастомный компонент (image в span), то для него по-любому придётся написать свой метод isSelected, и тут уж неважно, используете ли вы Selenium или Selenide. И ни в какие дебри лезть не надо. 

Так в этом же и был вопрос. Покажите как можно реализовать кастомные элеметы, не влезая в core фреймворка.
Например isSelected актуален только для чекбоксов и иеже с ними и т.д.

Допустим, если чекбокс у вас реализован как span, у которого может быть класс "on" (и тогда на нём CSS'ом рисуется картинка галочка) или "off" (и тогда на нём CSS'ом рисуется картинка с пустым квадратикком). 

Тогда можно проверить так:

$("#my-span").shouldHave(cssClass("on")).

 

А если потребуется какая-то мощная кастомизация, можно просто создать свой подкласс Condition:

 

$("#my-span").shouldBe(on).

И отдельно:
Condition on = new Condition() {
  @Override
  public boolean apply(WebElement element) {
    return hasClass(element, "on");
  }
}

Библиотека интересная, но есть моменты при написании тестов в которых надо сделать кастомизацию самим вебдрайвером.

Например выполнение различних javascript, или Actions c разными KeyPress и MouseOver, MouseOut, работа с iFrame і т.д.

Допускает ли использование вашей библиотеки обращение к драйверу дефолтними методами?

 

Еще один вопрос по инициализации браузера. Возможно ли, используя библиотеку, инициализировать удаленний браузер с помощю Selenium Grid?

Естественно хабы и узлы будут подняты на стороне настройки среды.

Вопрос в том удасться ли мне вызвать браузер на конкретных узлах используя нужные DesiredCapabilities и при этом не залезая в дебри структуры вашей библиотеки?

Целый ряд вопросов, спасибо. Отвечаю по порядку.

1. Да, библиотека Selenide позволяет использовать все методы самого Selenium, в т.ч. объект Webdriver. 

Делается просто:

import static com.codeborne.selenide.WebDriverRunner.getWebDriver

И дальше можно спокойно использовать метод getWebDriver(), который возвращает объект WebDriver. С ним можно делать всё, что вы привыкли. 

2. Но справедливости ради надо отметить, что все описанные вами действия можно сделать короче методами Selenide.

  - Для вызова JavaScript:     executeJavaScript("console.log('Hello')");

  - Для использования Actions: actions().click($(#rememberMe")).build().perform();

- Для переключения в iFrame: switchTo().frame($("#myFrame").toWebElement());

 

3. Да, можно использовать Selenium Grid. Для этого надо запустить Selenide с ключом "-Dremote=true -Dbrowser=chrome".

4. По умолчанию Selenide создаёт объект WebDriver в зависимости от системного свойства "browser" ("firefox", "htmlunit", "chrome", "ie", "opera", "phantomjs"). 

По умолчанию firefox. 

Если хочется какой-то другой браузер, можно передать имя класса: -Dbrowser=org.openqa.selenium.firefox.FirefoxDriver

Если хочется кастомизировать создание вебдрайвера, можно просто вызвать метод WebDriverRunner.setWebDriver(myWebDriver).

Ещё один вариант - передать при старте имя класса-фабрики -Dbrowser=com.mycompany.WebdriverFactory

Он должен рализовывать интерфейс com.codeborne.selenide.WebDriverProvider с одним-единственным методом WebDriver createDriver();

 

Пожалуй, стоит всё это куда-нибудь в Wiki записать...

 

2 лайка

Спасибо, все предельно ясно.

Для того чтоб мои старые тесты работали и постепенно можно было перевести код под selenide мне нужно после инициализации моего драйвера сделать назначение:

WebDriverRunner.setWebDriver(myWebDriver);

таким образом я смогу по старому делать старый и новый код не изменяя ничего в тестах, которые уже написаны:

myWebDriver.get('http://www.google.com')

open('http://www.google.com')

Оба должны работать.

 

 

Если хочется какой-то другой браузер, можно передать имя класса: -Dbrowser=org.openqa.selenium.firefox.FirefoxDriver

Здесь если можно приведите пример - где именно передавать имя класса по указаному примеру.

 

Еще раз спасибо.

Это зависит от того, как вы запускаете тесты.

Если из командной строки, то так:  java -Dbrowser=chrome -classpath junit-rt.jar... org.junit.Runner...

Если из IDE, то там в run configuration есть поле "VM Options" или что-то подобное.

Если из ANT скрипта, то 

 

      <junit>
        <jvmarg value="-Dbrowser=ie"/>
        <jvmarg value="-Dtimeout=6000"/>
        <batchtest todir="test-results">
          <fileset dir="${classes-test}" includes="@{classes}" excludes="**/Abstract*"/>
          <formatter type="xml"/>
        </batchtest>
        <classpath>
          <pathelement path="${classes-test}"/>
          <path refid="test.lib.path"/>
        </classpath>
      </junit>
 

Т.е. в общем случае будет энное количество Conditions, которые имеют одно и тоже значение (isSelected), но отличаются реализацией, например

1. Checkbox/Radiobutton (<img>)

2. Checkbox/Radiobutton (@style)

3. Checkbox/Radiobutton (<someattribute>)

4. Grid Row/Cell (@style)

5. Drowdown list option (<someattribute>)

6. Listbox item (@style)

и т.д. Причем как и @style, так и аттрибут может варьироваться в зависимости от браузера. А если к этому добавить еще и различные вариации  isDisplayed, isEnabled - получится довольно внушительный список. Причем каждый из этих Conditions может невозбранно быть применим к тем элементам, для которых он не имеет смысла.

Но... я очень ленив, и хотел бы иметь возможность экстендить стандартный элемент и оверрайдить в нем все что необходимо, что б потом не задумываясь писать везде и всюду shoudBe(visible()). А вот для этого придется лезть "внутрь".

И дело тут не только в этих Conditions, но и в экшенах в том числе. Например для некоторых элементов надо перед кликом надо непременно сделать mouseMove, а для некоторых и слипнуть секундочку, что б onfocus() успел отработать. А противный IE не скролит на опцию в "красивой" ddl, и надо "насильно" делать arguments[0].scrollintoView(). А Safari наотрез отказывается делать  hover у менюшки, хочет тока через js. Да и FF тоже не без грехов. И таких "затычек" превеликое множество, и все они специфичны для определенных контролов/браузеров.

Вот и хочется чего-то простого и гибкого, аля class Car{}; class Opel extends Car{}; class Astra extends Opel{}, что бы можно было без труда на пальцах объяснить что/где/зачем студентам/индусам/ньюкамерам. У вас же, за внешней видимой простотой, внутри все довольно не просто.

Если я вас правильно понимаю, писать не задумываясь не получится.

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

$("#my-element1").shoudBe(on);

$("#my-element2").shoudBe(enabled);

$("#my-element3").shoudBe(selected)

 

При вашем подходе, если я правильно понял, condition будет один, но зато разные элементы:

getSelectbox("#my-element1").shoudBe(visible);

getSpanImage("#my-element2").shoudBe(visible);

getRadioSpan("#my-element3").shoudBe(visible)

 

И в том, и в другом случае надо в каждой строке думать, какой элемент или какой Condition использовать. Кода ровно столько же.

А преимущество Selenide я здесь вижу в том, что вам как раз-таки не надо лезть в дебри и расширять базовый класс. Нужно только реализовать самому интерфейс Condition. По-моему, вот это как раз гибко. 

 

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

$("id1").shouldBe(checkboxDisabled);

$("id1").shouldBe(checkboxSelected);

$("id2").shouldBe(tabHeaderDisabled);

$("id2").shouldBe(tabHeaderSelected);

$("id2").shouldBe(tabHeaderMovable);

$("id3").shouldBe(gridRowSelected);

Ну да ладно. На вкус и цвет...

А как же быть с экшенами? Вот простой пример с HTMLEditor:

public void type(String value) {
click();
switch (BROWSER) {
case SAFARI:
executeScript("arguments[0].innerText='" + text + "'", this);
break;
default:
sendKeys(value);
}
}
 
Пока мне видится один не очень элегантный вариант, что-то типа 
$("id1").shoudleBe(HTMLEditorEnabled)
$("id1").shoudleBe(HTMLEditorEditable)
$("id1").typeInHTMLEditor("some text")
 
Хотелось бы увидеть пример тестов, но не синтетических (а-ля gmail, google) - а реального проекта, где написано уже порядка сотни-другой тестов.

Так первый способ обладает ровно теми же недостатками. 

Количество кондишенов может перевалить за сотню?

Ну, это вы загнули. Вы хотите сказать, что в вашем проекте чекбокс рисуется с помощью span и img сотней разных способов? Не верю.

А если так, то вам стоит поговорить с программерами, чтобы они сделали это как-нибудь попроще. Пусть у всех чекнутых чекбоксов будет класс "checked", как бы они там ни отрисовывались. Тестер при тестировании приложения должен думать о бизнес-логике, а не о том, какой из 100 способов отрисовки чекбокса выбрать для каждого элемента. 

 

 

С экщенами просто: сделайте свой static метод public static void type(String cssSelector, String value) {... всё, что вы написали ...}

и вызывайте его откуда угодно и когда угодно. Не надо лезть ни в какие дебри. 

 

Пример тестов из реального проекта вам никто никогда не покажет, ишь чего захотели. Это же секретная информация. 

К счастью, один наш клиент разрешил показать их код на конференции. Видео есть в блоге Selenide: http://ru.selenide.org/2013/05/09/video-selenide-on-seleniumcamp/

Там, конечно, всего проекта не видно, только чуть-чуть тестов, но зато они вполне реальные. 

Ну вот, записал в Wiki:

https://github.com/codeborne/selenide/wiki/How-Selenide-creates-WebDriver

 

1 лайк

Не вижу этого раздела у вас в оглавлении:

https://github.com/codeborne/selenide/wiki

Он доступен только по прямой ссылке?

Точно. Спасибо, добавил ссылку в оглавление.

Хотя мне до сих пор непонятно, нафига нужен RemoteWebDriver. По той же причине, что п. 1.

RemoteWebDriver+Grid в первую очередь нужен для масштабирования  выполнения тестов в рамках одного браузера. Исходя из моего опыта, среднее время выполнения теста составляет 1-2 минуты. Т.е. уже при наборе тестов в количестве около сотни придется задуматься о параллелизации. Добавим сюда мульти[браузерное/серверное] тестирование и станет ясно, что альтернатив этой связке нету.

Например:

Имеем пять тестовых машин и набор тестов, которые идут в:

IE - 30 min

FF - 20 min

Chrome - 15 min

В случае "по-браузерного" запуска репорт будет получен через 30 минут. В случае использования грида - через 6 минут.

Не надо лезть ни в какие дебри. 

Эмм, ну как же убедить что придется? Ну хотя бы потому, что в коде "косяков" вагон и маленькая тележка.

private void followLink(WebElement link) {
String href = link.getAttribute("href");
link.click();

// JavaScript $.click() doesn't take effect for <a href>
if (href != null) {
open(href);
}
}

open после click - это нормально?

 

protected void fireEvent(final String event) {

причем тут document.activeElement?

 

} catch (WebDriverException elementDoesNotExist) {
return false;

т.е. в случае краша браузера/не валидного локатора и прочих нештатных ситуаций exists будет послушно возвращать false, умалчивая о причинах, а всяческие wait'ы - ждать.

 

private static void waitUntilAlertDisappears() {

это дед-код - ничего он ждать не будет

 

 ieCapabilities.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, true);

так делать очень плохо

 

By.xpath(".//*[contains(normalize-space(text()), \"" + elementText + "\")]")

не совсем корректный xpath

и т.д.

Ну, это вы загнули. Вы хотите сказать, что в вашем проекте чекбокс рисуется с помощью span и img сотней разных способов? Не верю.

Сотней - нет. С пяток вариантов насчитать могу.

А если так, то вам стоит поговорить с программерами, чтобы они сделали это как-нибудь попроще.

Угу. Они с радостью перелопатят весь код проекта, которому уже за пять лет перевалило, и попросят задание посложнее :)

 

Еще раз повторюсь все эти прокси, делегаты, инвокейшен хендлеры здесь никакой полезной работы не делают, а напротив все усложняют и запутывают. По сути весь код может вполне себе уместиться в пару тройку классов.

Да я понимаю, зачем нужен параллельный запуск. Я говорю о том, что то же самое можно сделать проще. 

Например, Gradle умеет сам запускать тесты в нескольких параллельных процессах. Это на одной машине. А если хочется запустить тесты на нескольких машинах, то проще запустить их на Jenkins с несколькими нодами. 

Поговорив с народом, я понял, что Selenium Grid просто альтернатива этому варианту. Для тех, кто с Jenkins незнаком, а с Grid знаком, вполне себе вариант.

 

По "косякам":

1. followLink: да, open после click вполне нормально. Там же в комментарии вроде понятно написано, что в определённых услоиях просто click на элементе не срабатывает, и тогда мы и делаем open. Не нужно - не используй.

 

2. fireEvent: document.activeElement - это элемент, на котором в данный момент находится фокус. И на этом элементе нам нужно сгенерировать событие onChange или onClick. Что смущает?

3. exists() - да, функция возвращает false в случае любых крэшей, в этом и есть её предназначение. Её не нужно использовать во всяческих wait'ах. Для ожидания есть другия функции - например, $("input").should(exist) - вот она-то подождёт и сообщит о причинах.

4. waitUntilAlertDisappears: ну я не знаю, у меня используется в реальном проекте и вполне себе работает. Как только alert пропадает, вываливается NoAlertPresentExceptionА что с ним не так, я не очень понял?

5. Почему INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS - плохо? Я наоборот, нашёл в интернете дофига советов его использовать.

6. Про xpath не понял, почему он некорректный. Работает же. 

 

Внутри Selenide использован механизм Reflection API, который действительно нетривиальный. Нужен он был для того, чтобы создать объект WebElement с несколькими дополнительными методами. Туда просто не надо лезть. Вы же не лезете внутрь своего телефона, вы его просто используете. 

 

"По сути весь код может вполне себе уместиться в пару тройку классов." - ну напишите, с удовольствием посмотрим. :)