Введение
Последние три месяца мне пришлось работать с 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(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
private static final String SESSION_COOKIE_NAME = "JSESSIONID";
private static final String DOMAIN = "domain.here.com";
private static final String COOKIE_PATH = "/cookie/path/here";
protected boolean 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) {
return false;
}
}
protected BasicClientCookie getSessionCookie() {
Cookie originalCookie = webDriver.manage().getCookieNamed(SESSION_COOKIE_NAME);
if (originalCookie == null) {
return null;
}
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;
}
Очистка значения поля input
Проблема:
Порой, требуется выполнить очистку значения поля типа input. К примеру, необходимо заменить старое значение новым.
WebDriver предоставляет для этого специальный метод:
webElement.clear();
По моему опыту данный метод не работает, более того выбрасывает ошибку и ломает тест.
Требуется найти другой способ очищения значения поля.
Решение:
Есть несколько основных способов.
Первый способ — моделировать действие «выделить все» и сразу после этого выслать новое значение:
inputElement.sendKeys(Keys.chord(Keys.CONTROL, "a") + Keys.DELETE + newValue);
Но данное решение у меня работает не на всех браузерах и не всегда.
Второй способ — выслать количество символов backspace, равное длине старого значения. Данное решение некрасиво, но оно эффективно и работает гарантированно во всех браузерах.
Ниже я публикую вариант, которым пользуюсь сам. В нем есть отдельное рассмотрение ситуации, когда браузер — IE, а input с типом file.
Это особенная ситуация. При выполнении команды sendKeys к такому элементу IE заменит старое значение, новым, а не допишет его в конец. Поэтому делать очистку такого поля смысла нет. Более того, подобная попытка приведет к ошибке. Либо по причине несуществующего файла (так как будет попытка найти файл по пустому пути), либо по причине попытки найти файл по пути, строковое значение которого будет равно символу backspace.
Пример кода:
protected void clearInput(WebElement webElement) {
// isIE() - just checks is it IE or not - use your own implementation
if (isIE() && "file".equals(webElement.getAttribute("type"))) {
// workaround
// if IE and input's type is file - do not try to clear it.
// If you send:
// - empty string - it will find file by empty path
// - backspace char - it will process like a non-visible char
// In both cases it will throw a bug.
//
// Just replace it with new value when it is need to.
} else {
// if you have no StringUtils in project, check value still empty yet
while (!StringUtils.isEmpty(webElement.getAttribute("value"))) {
// "u0008" - is backspace char
webElement.sendKeys("u0008");
}
}
}
Загрузка файла на сервер
Проблема:
Необходимо загрузить файл на сервер, используя стандартные элементы HTML:
<input name="uploadFile" type="file">
<input name="doUpload" value="Upload" type="button"/>
Решение:
Я рекомендую просто взять и вынести себе это в отдельный универсальный метод и использовать его каждый раз когда нужно что-либо загрузить через такую форму.
Исключение:
Safari Driver полностью не поддерживает загрузку файла, т.к. насколько я понял, он javascript-based. Появляющееся окно с выбором файла ставит его в тупиковое положение. Такие сценарии нужно либо избегать, либо добиваться результата другим способом — создавать собственный HTTP запрос или подкладывать данные напрямую на стороне сервера, если такая возможность имеется.
Пример кода:
protected void 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 и как это делать эффективно.
Но все же… Я не хотел бы в будущем сталкиваться с задачами такого рода. Это крайне утомительно, отладка подобна мукам, заставляет порой писать плохой код, но больше всего страшно, что тестируемый web-клиент будет видоизменен. Я гарантированно знаю, что он будет изменен. И это еще один болезненный момент.
Поэтому, если вы решите писать серьезные тесты на этой платформе — подготовьтесь психологически.
Автор: Gadget
За три месяца пользования инструментом вы так и не научились правильно с ним работать. Тесты выполняются под управлением некого тестового фрейморка, в вашем случае это JUnit. Большинство из таких фремворков поддерживают многопоточность в запусках тестов. Т.е. ваша проблема про медленность высосана из пальца. Если еще учесть что вы знаете что такое Grid, то тем более странно что вам мешало запустить тесты на нескольких машинах. Если касаться потребления ресурсов, понятно оно будет немалым, по сути немного больше потребления браузера. Что касается зависимости между тестами, то тут проблема не Selenium, а архитектуры вашего решения. Понятно, что WebDriver не идеален, но на текущий момент это единственно стабильное и бесплатное решение на рынки, которое в умелых руках выполнит поставленную задачу. Поверьте моему 3-х летнему опыту работы с ним.