Русский /

Блог

В последнее время мне пришлось работать с Selenium 2.0 (WebDriver). В данной статье я опишу свои впечатления, мысли и опыт, который я приобрел. Так же я опишу основные действия, которые чаще всего вызывают проблемы и покажу наиболее удачные решения, которые я смог реализовать для них.

Коротко о Selenium

Библиотека Selenium позволяет производить тестирование графических web интерфейсов. Ее принцип — максимально точно симулировать деятельность пользователя. По сути — это написание бота, который бегает по страничкам сайта, производит действия и проверяет ожидаемый результат. Selenium 2.0 реализует сообщения с браузерами посредством специальных драйверов. В отличие от Selenium 1.0 он не использует JavaScript, а сообщается напрямую с API браузеров.

Что у меня получилось реализовать

Получилось написать тесты на основе JUnit и Selenium 2.0, объединенные в одно приложение. Данное приложение может выполняться на Selenium Grid — это сеть, во главе с Selenium Hub, который принимает и распределяет поступившие задачи тестирования по своим Selenium Nodes. На различных Selenium Nodes могут быть настроены любые требуемые браузеры. Используемые драйвера — родные драйвера для каждого браузера.

Часть первая. Впечатления

Разное поведение в разных браузерах

Под браузерами я понимаю основные: Firefox, Google Chrome, Opera, Safari, IE8, IE9. Чтобы один и тот же код срабатывал одинаково успешно в разных браузерах, нужно потратить огромное количество времени. Порой, нужна железная воля, чтобы не бросить это гиблое дело. В этом плане самые послушные браузеры — Firefox и Google Chrome. По моему личному опыту — жизненно необходимо, чтобы тест мог менять поведение в нужных местах в зависимости от того, какой браузер в данный момент используется. Т.е. он должен иметь информацию о среде, в которой он проходит.

Совет! Старайтесь не использовать в тестах объект webDriver напрямую! Лучше создавать методы-обертки вокруг основных необходимых вам методов. Проще менять поведение в одном месте, чем повсеместно в коде всех тестов.

Selenium 2.0 — сырой продукт

Читая множество постов на Stackoverflow в поисках best practices или просто решения проблемы, постоянно натыкаешься на workaround’ы. Причин несколько: различия в работе драйверов браузеров, не выполнение драйверами контракта требуемого функционала, наличие ошибок в версиях, наличие прямой зависимости версии браузера от версии драйвера. Порой он способен просто ронять тест на голом месте (с точки зрения пользователя API) — элемент есть, но он его не видит. По моему опыту много плавающих ошибок, которые намеренно воспроизводятся только через раз при абсолютно схожих условиях и действиях. С Firefox вообще иногда начинается лихорадка и браузер может просто закрыться с примерно таким сообщением: Error communicating with the remote browser. It may have died. Крайне тяжело найти причину, если она вообще доступна пользователю Selenium. По этому порой ситуация беспомощная — просто не работает функционал.

Совет! Такой прискорбный расклад дел заставляет менять поведение тест-кейса. К счастью, одни и те же вещи в GUI клиентах зачастую можно выполнить либо разной последовательностью, либо разным способом. В случае если вы не смогли найти решения погуглив — попытайтесь подобрать другое поведение, которое будет успешно отработано. Не замыкайтесь на конкретном действии, если на то нет особой необходимости.

Тесты Selenium — зависимые тесты

Это означает, что если дополнительно не позаботиться, действия одного теста могут влиять на результат другого теста. Это вполне очевидно, пользователь в том числе изменяет данные в процессе своей активности. Тестируя подобный функционал, вы вынужденно будете изменять начальные данные. Если от этого зависят другие тесты и вы не вернули данные в исходное состояние — или не смогли этого сделать по причине того, что тест прервался ошибкой — другой тест может тоже сломаться. Этакий принцип домино. Когда впервые осознаешь это, становится очень больно. Руки опускаются…

Совет! Если есть возможность воспроизводить условия теста независимо, т.е. есть прямой доступ к тестируемому приложению и нет преград для разворачивания исходных тестовых данных — вам повезло, изолируйте свои тесты подобным образом — подготавливая данные до теста и очищая их после. К примеру, в восстановлении данных в БД может помочь инструмент Liquibase.

Скорей всего такой возможности нет. В этом случае выход один — помимо самих тестируемых действий, описывать с помощью Selenium так же и действия по их «откатке». Т.е. если пользователь удалил сущность — в конце теста ее необходимо заново создать или загрузить.

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

Тесты Selenium — медленные тесты

Нужно быть готовым, что последовательный прогон большого набора тестов для всех браузеров может занимать большое количество времени, измеряемое в часах (от 30 минут — до 2-3 часов). Это делает все что я описал выше трагедиями и порой похоже на издевательство. Причина в том, что тесты сильно насыщены различными ожиданиями, поиском элементов и прочими медленными действиями.

Совет! Тестируйте только то, что действительно надо тестировать. Из всех возможных работающих вариантов реализации одного и того же действия — выбирайте наиболее быстрый.

Selenium IDE — не помощник

Selenium IDE — специальный плагин для Firefox, который способен записать все выполняемые действия пользователя в виде скриптов. Там же есть возможность экспорта составленных скриптов в различные языки и в два формата: Selenium 1.0 (RC) и Selenium 2.0 (WebDriver).

В большинстве случаев бесполезная вещь.

Проблемы:

  • генерируемый код — не читаем
  • генерируемый код — не рабочий, в случае сложного интерфейса, в силу всех вышеописанных особенностей
  • в случае если id элементов (div, table, span, input) автоматически генерируемые — предлагаемые на выбор XPath указатели не подходят
  • большое количество тестов (5 тестов уже достаточно) заставит встать на правильный путь джедая и составить собственную реализацию часто выполняемых действий — и далее использовать их как унаследованный метод. Единожды описанный и отточенный. Как только образуется такой набор методов — польза от IDE резко падает. Ей нельзя указать использовать свои методы — среда разработки будет генерить свои собственные неработающие неидеальные шаблоны. Просматривать потом сгенерированный код и замещать все необходимые места со временем сводится к полному переписыванию этого кода. Эту же мысль можно продолжить единым «справочником»-перечнем всех XPath локаторов ключевых элементов. Как только все подобные локаторы вынесены в константы или в отдельный справочник — проще становится использовать их, чем в очередной раз проверять — что там нагенерила среда разработки

Польза — единственная польза, которую я постоянно ощущаю — возможность проверить XPath указатель. Очень удобная функция — если указатель верный и такой элемент существует — он подсвечивается рамочкой.

Совет! Поиграйтесь с IDE, поймите суть Selenium, можете даже пописать тесты с ее помощью. Но как только почувствуете, что пользы меньше, чем затрат — начинайте делать собственные заготовки. Аккумулируйте их в общем абстрактном классе-предке или в утилитном классе. Начиная с определенного момента ваши тесты могут превратиться просто в перечисление таких методов, разбавленные проверками результата и текущего состояния.

Часть вторая. Практические решения возникающих проблем (Java)

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

Получение элемента (findElement)

Проблема — WebDriver предоставляет механизм для поиска и получения сущности WebElement: webDriver.findElement(By.id(«elementId»));

Теоретически, на поведение этого метода влияет параметр ‘implicit wait’ который можно указать при построении самого webdriver. К примеру, так:

webDriver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);

Опять таки, теоретически, это должно явно заставлять webdriver искать элемент в течении указанного времени и ждать либо пока не появится искомый элемент, либо пока не закончится указанный таймаут. Кстати, данный таймаут судя по всему можно выставлять только единожды.

На практике, происходит нечто странное. Пауза выдерживается, но есть внутреннее ощущение, что поиск если и идет по DOM модели, то обновления этой DOM модели не происходит. Для некоторых браузеров получается другая ситуация — элемент уже есть в DOM модели, но еще не отрисовался или отрисовался частично (Google Chrome). WebDriver возвращает найденный наполовину отрисованный элемент и событие click попадает в еще неотрисованные координаты. Метод isDisplayed() в таких случаях не помогает. В любом случае, итог у меня всегда один — элемент визуально уже гарантированно появился, а webDriver его по-прежнему не обнаруживает.

Решение — делать грубую паузу. Для того, чтобы не умножать количество строк кода вдвое, рекомендую сделать собственную реализацию метода findElement();

Как я писал выше — для более эффективной работы — тест должен знать какой браузер в данный момент запущен. Для Firefox по моим наблюдениям такой задержки не требуется. Так же можно воспользоваться инструментом WebDriverWait. Описывать здесь такой вариант не буду, так как я решил остановиться на спячке потока, мне этого достаточно — поэтому проверенного варианта нет. Но там все довольно просто. В дальнейшем использовать во всех тестах только этот метод и не использовать webDriver.findElement() напрямую.

Пример кода:

protected WebElement findElement(By elementLocatorToFind) {
if(isSafari() || isChrome() || isIE()) {
// for example, use simple Thread.sleep(1000) inside
doDelayForMilliseconds(
1000);
}

return webDriver.findElement(elementLocatorToFind);
}

Получение элементов (findElements)

Проблема и решение аналогичны поиску одного элемента.

Проверка на существование элемента

Если необходимо проверить, что элемент отсутствует — рекомендуется использовать конструкцию:

findElements(elementLocatorToFind).isEmpty();

Вот рекомендация из JavaDocs:

findElement should not be used to look for non-present elements, use findElements(By) and assert zero length response instead.

Скачивание картинки или файла

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

Рассуждения — в 99% случаев вам это не нужно. Еще раз задайтесь вопросом, что вы хотите протестировать? Я практически уверен, что вам достаточно знать, что загрузка доступна. Что ссылка активна, кнопка загрузки enabled и статус ответа после начала скачки равен 200. У вас нет задачи тестировать браузер и его процесс скачивания.

Так же, если тесты проходят на Selenium Grid — то у вас не получится скачать файл и проверить после этого его местонахождение. Файл скачается на Selenium Node, а проверять вы его будете на Selenium Hub. Это разные хосты, по крайней мере в обычной практике.

Решение заключается в том, чтобы выполнить обыкновенный HTTP запрос по ссылке ведущей к файлу на сервере, либо по ссылке, по которой сервер должен вернуть такой файл. Если статус полученного от сервера ответа 200 — ссылка верная, файл существует. Все остальные варианты я рассматриваю как недоступность скачивания файла. Так как зачастую запросы должны иметь при себе авторизованные cookies — такие cookies необходимо импортировать из webDriver.

Если одного статуса недостаточно, ничто не мешает вам считать весь InputStream из HttpEntity и далее сравнить его содержимое с эталонным, будь то MD5 сумма или какой-нибудь другой способ.

Пример кода:

// just look at your cookie’s content (e.g. using browser) and import these settings from it
   
privatestaticfinal String SESSION_COOKIE_NAME = «JSESSIONID»;
   
privatestaticfinal String DOMAIN = «domain.here.com»;
   
privatestaticfinal String COOKIE_PATH = «/cookie/path/here»;

   
protectedboolean isResourceAvailableByUrl(String resourceUrl) {
       HttpClient httpClient =
new DefaultHttpClient();
       HttpContext localContext =
new BasicHttpContext();
       BasicCookieStore cookieStore =
new BasicCookieStore();
       cookieStore.addCookie(getSessionCookie());
       localContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);
       
// resourceUrl — is url which leads to image
       HttpGet httpGet =
new
HttpGet(resourceUrl);

       
try {
           HttpResponse httpResponse = httpClient.execute(httpGet, localContext);
           
return httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
       }
catch (IOException e) {
           
returnfalse
;
       }
   }

   
protected BasicClientCookie getSessionCookie() {
       Cookie originalCookie = webDriver.manage().getCookieNamed(SESSION_COOKIE_NAME);

       
if (originalCookie == null) {
           
returnnull
;
       }

       String cookieName = originalCookie.getName();
       String cookieValue = originalCookie.getValue();
       BasicClientCookie resultCookie =
new BasicClientCookie(cookieName, cookieValue);
       resultCookie.setDomain(DOMAIN);
       resultCookie.setExpiryDate(originalCookie.getExpiry());
       resultCookie.setPath(COOKIE_PATH);
       
return resultCookie;
   }

Загрузка файла на сервер

Проблема — необходимо загрузить файл на сервер, используя стандартные элементы HTML:

<input name=«uploadFile»type=«file»>
<input name=«doUpload»value=«Upload»type=«button»/>

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

Исключение — Safari Driver полностью не поддерживает загрузку файла, т.к. насколько я понял, он javascript-based. Появляющееся окно с выбором файла ставит его в тупиковое положение. Такие сценарии нужно либо избегать, либо добиваться результата другим способом — создавать собственный HTTP запрос или подкладывать данные напрямую на стороне сервера, если такая возможность имеется.

Пример кода:

protectedvoid uploadFile(By uploadInput, By uploadButton, String filePath) {
       clearInput(uploadInput);
       findElement(uploadInput).sendKeys(filePath);
       findElement(uploadButton).click();
}

Действия с элементами внутри iframe

Проблема — Если требуемый элемент находится внутри iframe — он не доступен из дефолтного контекста. Вы не сможете обнаружить его в DOM модели и webDriver будет бросать исключение NoSuchElementException.

Решение — Перед тем как взаимодействовать с этим элементом, необходимо переключить webDriver на контекст iframe элемента. Как я понимаю, это связано с тем, что контекст страницы и контекст iframe на этой странице — это две разных DOM модели.

Пример кода:

webDriver.switchTo().frame(findElement(By.id(«id_of_your_iframe»)));
// do actions against inner web element, located in iframe
webDriver.switchTo().defaultContent();
// continue to do actions in default content

IE8. Проблема с XPath

Проблема — IE8, в свойственной ему эксцентричной манере, бывает неверно интерпретирует указатели элементов (By.id, By.xpath и другие).

У меня возникали ситуации когда он игнорировал уточнение для искомого элемента указывающее на его атрибут class.

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

findElement(By.xpath(«//div[@id='elementContainer']/div[@class='someProcessInProgress']«));
findElement(By.xpath(
«//div[@id='elementContainer']/div[@class='someProcessFinished']«));

Понять в каких ситуациях у него возникают проблемы мне не удалось.

Абсолютно идентичная ситуация происходила с прямым указанием id элемента. WebDriver делает вид что его не существует.

Решение — Если у IEDriver возникает галлюциногенное заблуждение в поиске элемента (но не у остальных драйверов и браузеров) — лучший выход — изменить XPath. Благо возможных вариантов благодаря гибкости XPath всегда много.

IE8. элемент недоступен для нажатия

Проблема — IE8, в отличие от остальных браузеров, не всегда способен самостоятельно выполнить прокрутку до элемента, если выполняется нажатие на элемент, находящийся за пределами видимой части контейнера (слоя, таблицы и прочего). В итоге такое поведение приводит к ошибке.

Решение — необходимо выполнить прокрутку. Единственный найденный мной и работающий способ — воспользоваться помощью javascript. На самом деле, WebDriver имеет специальный механизм, призванный помочь с прокруткой до требуемого элемента:

new Actions(webDriver).moveToElement(elementToScrollTo).perform();

Но он не сработает в случае с IE8.

Пример кода:

((JavascriptExecutor) webDriver).executeScript(«container.scrollLeft=1000;»);

Где container — это id элемента, который необходимо прокрутить. Т.е. в нашем случае — div или table, внутри которого располагается элемент. Как можно понять, данный скрипт выполнит прокрутку по горизонтали.

Firefox may die

Проблема — Firefox Driver мог быть примером для остальных драйверов, но у него есть один очень неприятный недостаток. Как можно понять по коментариям к разным версиям WebDriver’a — этот недостаток, то исчезает, то вновь появляется от версии к версии.

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

Выглядит это примерно так: вы наблюдаете, как в окне браузера успешно выполняется уже отлаженный до блеска тест. И тут на абсолютно мелочном шаге или действии окно браузера просто исчезает. В логах вы обнаруживаете такую запись: Error communicating with the remote browser. It may have died. Все, больше никакой информации вы не найдете.

Решение — это регрессивная ошибка и гарантированного лекарства быть не может. Заключается она в том, что между браузером и драйвером возникает непонимание. К примеру по причине того, что ваш браузер обновился, вы не обратили на это внимание и продолжаете использовать старый WebDriver. Между Firefox и его драйвером есть, как я это ощутил, зависимость. Она не абсолютная, т.е. не всякий раз когда Firefox обновился, нужно бежать обновлять вебдрайвер тоже. Но первое что я посоветую сделать — погуглите, какая версия вебдрайвера наиболее подходит под вашу версию Firefox.

В случае с Firefox 19 мне помогло обновить stand-alone-server селениума до версии 2.30.0.

Заключение

Я благодарен за такой опыт и за возможность поработать с этим фреймворком. За время работы с селениумом XPath для меня стал как родной язык, наверное скоро переписываться в мессенджерах на нем смогу. Судя по всему, я получил достаточно много знаний о том, как использовать Selenium и как это делать эффективно. Но все же, начиная знакомство с ним — подготовьтесь психологически :)

Максим Никитин