Привет, Хабр.
Меня зовут Виталий Котов, я работаю в отделе тестирования Badoo. Я пишу много UI-автотестов, но еще больше работаю с теми, кто занимается этим не так давно и еще не наступил на все ошибки.
Итак, объединив собственный опыт и наблюдения других ребят, я решил подготовить для вас сборник «как не надо писать тесты».
Каждый пример я сопровождал подробным описанием, примерами кода и скриншотами.
Статья будет интересна начинающим авторам UI-тестов, но старожилы в этой теме наверняка узнают что-то новое или просто улыбнутся, вспомнив себя «в молодости».
:)
Идти!
Содержание
- Локаторы без атрибутов
- Проверка отсутствия элемента
- Проверка внешнего вида элемента
- Случайные данные
- Тест атомарности (часть 1)
- Тест атомарности (часть 2)
- Ошибка при нажатии на существующий элемент
- Текст ошибки
- Нижняя граница
Локаторы без атрибутов
Начнем с простого примера.Поскольку мы говорим о UI-тестах, важную роль в них играют локаторы.
Локатор — это строка, составленная по определенному правилу и описывающая один или несколько элементов XML (в частности HTML).
Существует несколько типов локаторов.
Например, CSS-локаторы используется для каскадных таблиц стилей.
XPath-локаторы используется для работы с XML-документами.
И так далее.
Полный список типов локаторов, которые используются в Селен , можно найти по адресу seleniumhq.github.io .
В UI-тестах локаторы используются для описания элементов, с которыми должен взаимодействовать драйвер.
Практически в любом браузерном инспекторе есть возможность выбрать интересующий нас элемент и скопировать его XPath. Это выглядит примерно так:
В результате получается такой локатор:
Кажется, ничего страшного в таком локаторе нет. Ведь мы можем сохранить его в какой-нибудь константе или поле класса, которое своим именем будет передавать суть элемента:/html/body/div[3]/div[1]/div[2]/div/div/div[2]/div[1]/a
@FindBy(xpath = "/html/body/div[3]/div[1]/div[2]/div/div/div[2]/div[1]/a")
public WebElement createAccountButton;
И оберните его соответствующим текстом ошибки на случай, если элемент не найден:
public void waitForCreateAccountButton()
{
By by = By.xpath(this.createAccountButton);
WebDriverWait wait = new WebDriverWait(driver, timeoutInSeconds);
wait
.
withMessage(“Cannot find Create Account button.”)
.
until(
ExpectedConditions.presenceOfElementLocated(by)
);
}
Преимущество этого подхода состоит в том, что он устраняет необходимость изучения XPath. Однако есть и ряд недостатков.
Во-первых, если изменится макет, нет гарантии, что элемент, использующий такой локатор, останется прежним.
Вполне возможно, что его место займет кто-то другой, что приведет к непредвиденным обстоятельствам.
Во-вторых, задача автотестов — искать ошибки, а не следить за изменениями верстки.
Поэтому добавление какой-то обертки или каких-то других элементов выше по дереву не должно влиять на наши тесты.
В противном случае на обновление локаторов у нас уйдет довольно много времени.
Вывод: нам следует создавать локаторы, корректно описывающие элемент и в то же время устойчивые к изменениям макета вне тестируемой части нашего приложения.
Например, вы можете привязаться к одному или нескольким атрибутам элемента: //a[@rel=”createAccount”]
Такой локатор легче воспринимается в коде, и он сломается, только если исчезнет «rel».
Еще одним преимуществом такого локатора является возможность поиска в репозитории шаблона с указанным атрибутом.
На что обратить внимание, если локатор выглядит как в исходном примере? :) Если изначально в приложении элементы не имеют атрибутов или они заданы автоматически (например, из-за запутывание классов), это стоит обсудить с разработчиками.
Они должны быть не менее заинтересованы в автоматизации тестирования продуктов и обязательно пойдут вам навстречу и предложат решение.
Проверка отсутствия элемента
У каждого пользователя Badoo есть свой профиль.Содержит информацию о пользователе: (имя, возраст, фотографии) и информацию о том, с кем пользователь хочет общаться.
Кроме того, у вас есть возможность указать свои интересы.
Допустим, однажды у нас случился баг (хотя, конечно, это не так :)).
Пользователь выбрал интересы в своем профиле.
Не найдя в списке подходящего интереса, он решил нажать «Ещё», чтобы обновить список.
Ожидаемое поведение: старые интересы должны исчезнуть, должны появиться новые.
Но вместо этого выскочила «Неожиданная ошибка»:
Оказалось, что возникла проблема на стороне сервера, ответ был неверным, и клиент обработал эту проблему, показав соответствующее уведомление.
Наша задача — написать автотест, который проверит этот случай.
Пишем что-то вроде следующего скрипта:
- Открыть профиль
- Открыть список интересов
- Нажмите кнопку «Еще»
- Убедитесь, что нет ошибки (например, нет элемента «div.error»).
Однако происходит следующее: через несколько дней/месяцев/лет баг появляется снова, хотя тест ничего не ловит. Почему? Все довольно просто: при успешном завершении теста менялся локатор элемента, по которому мы искали текст ошибки.
Шаблоны были переработаны, и вместо класса error у нас теперь есть класс error_new. Во время рефакторинга тест продолжал работать как положено.
Элемент «div.error» не появился, причин для сбоя не было.
Но теперь элемента «div.error» вообще не существует — поэтому тест никогда не провалится, что бы ни происходило в приложении.
Вывод: лучше проверять работоспособность интерфейса положительными тестами.
В нашем примере ожидалось, что список интересов изменился.
Бывают ситуации, когда отрицательный чек невозможно заменить на положительный.
Например, при взаимодействии с каким-то элементом в «хорошей» ситуации ничего не происходит, а в «плохой» появляется ошибка.
В этом случае стоит придумать способ смоделировать «плохой» сценарий и написать для него тоже автотест. Таким образом мы проверим, что элемент ошибки появляется в отрицательном регистре, и тем самым проследим за актуальностью локатора.
Проверка внешнего вида элемента
Как убедиться, что взаимодействие теста с интерфейсом прошло успешно и все работает? Чаще всего это видно по изменениям, произошедшим в этом интерфейсе.Давайте посмотрим на пример.
Вам необходимо сделать так, чтобы при отправке сообщения оно появлялось в чате:
Скрипт выглядит примерно так:
- Открыть профиль пользователя
- Откройте с ним чат
- написать сообщение
- Отправлять
- Подождите, пока появится сообщение
Предположим, что сообщение чата имеет соответствующий локатор: p.message_text
Вот как мы проверяем, что элемент появился: this.waitForPresence(By.css(‘p.message_text’), "Cannot find sent message.");
Если наше ожидание работает, то всё в порядке: сообщения в чате рисуются.
Как вы уже догадались, через некоторое время отправка сообщений в чате прерывается, но наш тест продолжает работать без перебоев.
Давайте разберемся.
Оказывается, накануне в чате появился новый элемент: какой-то текст, предлагающий пользователю выделить сообщение, если оно вдруг осталось незамеченным:
И, что самое смешное, он тоже попадает под наш локатор.
Только у него есть дополнительный класс, отличающий его от отправленных сообщений: p.message_text.highlight
Наш тест не прервался при появлении этого блока, но проверка «ожидать появления сообщения» больше не имела значения.
Элемент, который был показателем успешного мероприятия, теперь всегда рядом.
Вывод: если логика тестирования основана на проверке внешнего вида какого-то элемента, то обязательно нужно проверять, что такого элемента не существовало до нашего взаимодействия с UI.
- Открыть профиль пользователя
- Откройте с ним чат
- Убедитесь, что нет отправленных сообщений
- написать сообщение
- Отправлять
- Подождите, пока появится сообщение
Случайные данные
Довольно часто UI-тесты работают с формами, в которые вводят определенные данные.
Например, у нас есть форма регистрации:
Данные для таких тестов могут храниться в конфигах или жестко закодироваться в тесте.
Но иногда в голову приходит мысль: а почему бы не рандомизировать данные? Это хорошо, мы рассмотрим больше случаев! Мой совет: не делайте этого.
И сейчас я расскажу вам, почему.
Допустим, наш тест зарегистрирован на Badoo. Мы решаем, что выберем пол пользователя случайным образом.
На момент написания теста процесс регистрации для девочки и мальчика ничем не отличается, поэтому наш тест проходит успешно.
Теперь представим, что через какое-то время процесс регистрации станет другим.
Например, мы дарим девушке бесплатные бонусы сразу после регистрации и уведомляем ее об этом специальной накладкой.
Тест не содержит логики закрытия оверлея, а это, в свою очередь, мешает некоторым дальнейшим действиям, прописанным в тесте.
Мы получаем тест, который проваливается в 50% случаев.
Любой инженер по автоматизации подтвердит, что UI-тесты по своей природе нестабильны.
И это нормально, с этим приходится жить, постоянно лавируя между избыточной логикой «на все случаи жизни» (которая заметно портит читаемость кода и усложняет его поддержку) и этой самой нестабильностью.
В следующий раз, когда тест провалится, у нас может не хватить времени разобраться с этим.
Мы просто перезапустим его и посмотрим, что он пройдет. Давайте решим, что в нашем приложении всё работает как надо и проблема в нестабильном тесте.
И давайте успокоимся.
Теперь давайте двигаться дальше.
Что, если эта накладка сломается? Тест продолжит проходить в 50% случаев, что существенно задерживает обнаружение проблемы.
И это хорошо, когда за счет рандомизации данных мы создаем ситуацию «50/50».
Но бывает и по-другому.
Например, раньше при регистрации допустимым считался пароль длиной не менее трёх символов.
Пишем код, который придумывает нам случайный пароль длиной не короче трех символов (иногда три символа, а иногда и больше).
И тут правило меняется – и пароль должен содержать не менее четырех символов.
Какую вероятность падения мы получим в этом случае? А если наш тест обнаружит настоящую ошибку, как быстро мы ее выясним? Особенно сложно работать с тестами, где вводится много случайных данных: имя, пол, пароль и так далее.
В этом случае тоже много разных комбинаций, и если в одной из них возникает ошибка, обычно это нелегко заметить.
Заключение.
Как я писал выше, рандомизация данных – это плохо.
Лучше покрыть больше случаев за счет поставщиков данных, не забывая и о классы эквивалентности , само собой.
Сдача анализов займет больше времени, но с этим можно справиться.
Но мы будем уверены, что если есть проблема, то она будет обнаружена.
Тест атомарности (часть 1)
Давайте посмотрим на следующий пример.Пишем тест, который проверяет счетчик пользователей в футере.
Скрипт прост:
- Открыть приложение
- Найти счетчик в нижнем колонтитуле
- Убедитесь, что это видно
Тогда возникает необходимость проверить, чтобы счетчик не показывал ноль.
«Мы добавляем эту проверку в уже существующий тест, почему бы и нетЭ» Но тогда возникает необходимость проверить, чтобы в футере была ссылка на описание проекта (ссылка «О нас»).
Стоит ли писать новый тест или добавить его к существующему? В случае нового теста нам придется заново запускать приложение, готовить пользователя (если мы проверяем футер на авторизованной странице), авторизоваться — в общем, терять драгоценное время.
В такой ситуации переименование теста в testFooterCounterAndLinks кажется хорошей идеей.
С одной стороны, у такого подхода есть преимущества: экономия времени, хранение всех проверок какой-то части нашего приложения (в данном случае футера) в одном месте.
Но есть и заметный недостаток.
Если тест не пройден в первом тесте, мы не будем тестировать остальную часть компонента.
Предположим, в какой-то ветке тест упал не из-за нестабильности, а из-за ошибки.
Что делать? Вернуть задачу, описывающую только эту проблему? Тогда мы рискуем получить задание, исправляющее только этот баг, запускаем тест и обнаруживаем, что следующий компонент тоже сломан, в другом месте.
И таких итераций может быть много.
Пинок билета туда-сюда в этом случае займет много времени и будет неэффективным.
Вывод: стоит по возможности распылять проверки.
В этом случае, даже если в одном случае у нас возникнет проблема, мы проверим все остальные.
А, если вам придется вернуть билет, мы сможем сразу описать все проблемные места.
Тест атомарности (часть 2)
Давайте посмотрим на другой пример.Мы пишем тест чата, который проверяет следующую логику.
Если у пользователей есть взаимная симпатия, в чате появляется следующий промоблок:
Скрипт выглядит следующим образом:
- Пользователь А голосует за пользователя Б
- Пользователь Б голосует за пользователя А
- Пользователь А открывает чат с пользователем Б.
- Подтвердите, что блок на месте
Нет, на этот раз тест не пропускает ни одной ошибки.
:) Через некоторое время мы узнаем, что есть еще один баг, не связанный с нашим тестом: если открыть чат, сразу закрыть его и открыть снова, блок исчезает. Это не самый очевидный случай, и мы его, конечно, не предусмотрели в тесте.
Но мы решаем, что нужно и это охватить.
Возникает тот же вопрос: написать еще один тест или вставить тест в существующий? Кажется бессмысленным писать новый, потому что в 99% случаев он будет делать то же самое, что и существующий.
И решаем добавить проверку в уже существующий тест:
- Пользователь А голосует за пользователя Б
- Пользователь Б голосует за пользователя А
- Пользователь А открывает чат с пользователем Б.
- Подтвердите, что блок на месте
- Закрыть чат
- Открыть чат
- Подтвердите, что блок на месте
Например, в проекте произойдет редизайн и придется переписать множество тестов.
Откроем тест и попробуем вспомнить, что он проверяет. Например, тест называется testPromoAfterMutualAttraction. Разберемся, почему открытие и закрытие чата написано в конце? Скорее всего нет. Особенно если этот тест писали не мы.
Стоит ли нам оставить этот кусок? Может и да, но если с ним возникнут проблемы, велика вероятность, что мы его просто удалим.
И чек потеряется просто потому, что его смысл не будет очевиден.
Я вижу здесь два решения.
Первое: все-таки сделать второй тест и назвать его testCheckBlockPresentAfterOpenAndCloseChat. По такому названию будет понятно, что мы не просто совершаем какой-то набор действий, а совершаем вполне осознанную проверку, поскольку у нас был негативный опыт. Второе решение — написать в коде подробный комментарий о том, почему мы делаем эту проверку именно в этом тесте.
Также желательно указать в комментарии номер ошибки.
Ошибка при нажатии на существующий элемент
Мне дали следующий пример ббидокс , за что получает большой плюс в карму! Бывает очень интересная ситуация, когда тестовый код становится.фреймворком.
Допустим, у нас есть такой метод: public void clickSomeButton()
{
WebElement button_element = this.waitForButtonToAppear();
button_element.click();
}
В какой-то момент с этим методом начинает происходить что-то странное: тест вылетает при попытке нажать на кнопку.
Мы открываем скриншот, сделанный в момент сбоя теста, и видим, что на скриншоте есть кнопка и метод waitForButtonToAppear успешно сработал.
Вопрос: что не так со щелчком? Самое сложное в этой ситуации то, что иногда проверка может быть успешной.
:) Давайте разберемся.
Предположим, что кнопка в примере расположена на наложении вот так:
Это специальная оверлей, посредством которой пользователь на нашем сайте может заполнить информацию о себе.
При нажатии на выделенную кнопку наложения появляется следующий блок для заполнения.
Просто ради интереса добавим для этой кнопки дополнительный класс OLOLO:
Затем нажимаем на эту кнопку.
Визуально ничего не изменилось, но сама кнопка осталась на месте:
Что случилось? Фактически, когда JS перерисовал за нас блок, он перерисовал и кнопку.
Доступ к нему по-прежнему осуществляется через тот же локатор, но это другая кнопка.
Об этом свидетельствует отсутствие добавленного нами класса OLOLO. В приведенном выше коде мы сохраняем элемент в переменной $element. Если за это время элемент перегенерируется, визуально это может быть не заметно, но кликнуть по нему уже не получится — метод click() завершится с ошибкой.
Есть несколько возможных решений:
- Оберните клик в блоке try и соберите элемент в catch.
- Добавьте к кнопке какой-нибудь атрибут, чтобы сигнализировать о том, что она изменилась.
Текст ошибки
Наконец, простой, но не менее важный момент. Этот пример касается не только UI-тестов, но и встречается в них очень часто.Обычно, когда вы пишете тест, вы находитесь в контексте происходящего: описываете тест за тестом и понимаете их смысл.
И тексты ошибок вы пишете в том же контексте: WebElement element = this.waitForPresence(By.css("a.link"), "Cannot find button");
Что может сбить с толку в этом коде? Тест ждет появления кнопки и, если ее нет, естественно вылетает.
А теперь представьте, что автор теста находится на больничном, а за тестами присматривает его коллега.
И тогда его тест testQuestionsOnProfile завершается неудачей и пишет следующее сообщение: «Невозможно найти кнопку».
Моему коллеге нужно как можно быстрее разобраться в том, что происходит, ведь скоро релиз.
Что ему придется делать?
Открывать страницу, на которой тест не прошёл, и проверять локатор «a.link» бессмысленно — элемента нет. Поэтому вам придется внимательно изучить тест и понять, что он проверяет.
Было бы намного проще с более подробным текстом ошибки: «Невозможно найти кнопку «Отправить» в наложении вопросов».
При такой ошибке можно сразу открыть оверлей и посмотреть, куда делась кнопка.
Есть два вывода.
Во-первых, стоит передать текст ошибки в любой метод вашего тестового фреймворка, причем в качестве обязательного параметра, чтобы не было соблазна о нем забыть.
Во-вторых, текст ошибки должен быть подробным.
Это не всегда означает, что оно должно быть длинным, достаточно, чтобы было ясно, что в тесте пошло не так.
Как определить, правильно ли написан текст ошибки? Очень просто.
Представьте, что ваше приложение сломалось и вам нужно подойти к разработчикам и объяснить, что и где сломалось.
Если им сказать только то, что написано в тексте ошибки, они поймут?
Нижняя граница
Написание тестового сценария часто оказывается интересным занятием.Мы преследуем множество целей одновременно.
Наши тесты должны:
- охватить как можно больше дел
- работать как можно быстрее
- будь понятен
- просто расширь
- простой в обслуживании
- заказать пиццу
- и так далее…
Именно поэтому стоит заранее продумать некоторые моменты и не всегда спешить с решениями.
:) Надеюсь, мои советы помогут вам избежать некоторых проблем и заставят более вдумчиво подойти к подготовке дел.
Если статья понравится публике, я постараюсь собрать еще несколько интересных примеров.
А пока – пока! Теги: #Selenium #qa #appium #автоматическое тестирование #java #мобильное тестирование #веб-тестирование #тестирование ИТ-систем #java #Тестирование веб-сервисов #Тестирование мобильных приложений
-
15 Распространенных Мифов О Программировании
19 Oct, 24 -
Варианты Использования Cisco Eem
19 Oct, 24 -
Снимаем «Матрицу» Дома На 15 Камер Gopro.
19 Oct, 24 -
Нестандартный Реферер В Опере-Мини
19 Oct, 24 -
Работаем С Менеджерами Удаленно
19 Oct, 24 -
Привет Блогерам От Льва Николаича.
19 Oct, 24