Хабрахабр

UI-автотесты: как делать не стоит

Здравствуй, Хабр. Меня зовут Виталий Котов, я работаю в отделе тестирования компании Badoo. Я пишу много UI-автотестов, но ещё больше работаю с теми, кто занимается этим не так давно и ещё не успел наступить на все грабли.

Каждый пример я подкрепил подробным описанием, примерами кода и скриншотами. Итак, сложив свой собственный опыт и наблюдения за другими ребятами, я решил подготовить для вас коллекцию того, «как писать тесты не стоит».

🙂 Статья будет интересна начинающим авторам UI-тестов, но и старожилы в этой теме наверняка узнают что-то новое, либо просто улыбнутся, вспомнив себя «в молодости».

Поехали!

Содержание

Локаторы без атрибутов

Начнём с простого примера. Так как мы говорим о UI-тестах не последнюю роль в них играют локаторы. Локатор — это строка, составленная по определённому правилу и описывающая один или несколько XML- (в частности HTML-) элементов.

Например, css-локаторы используются для каскадных таблиц стилей. Существует несколько видов локаторов. И так далее. XPath-локаторы используются для работы с XML-документами.

Полный список типов локаторов, которые используются в Selenium, можно найти на 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.");

Если наш wait работает, то всё в порядке: сообщения в чате отрисовываются.

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

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

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

Элемент, который был индикатором удачного события, теперь есть всегда. p.message_text.highlight

Наш тест при появлении этого блока не сломался, но проверка «дождаться появления сообщения» перестала быть актуальной.

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

  • Открыть профиль пользователя
  • Открыть чат с ним
  • Убедиться, что отправленных сообщений нет
  • Написать сообщение
  • Отправить
  • Дождаться появления сообщения

Случайные данные

Довольно часто UI-тесты работают с формами, в которые они вносят те или иные данные. Например, у нас есть форма регистрации:

Но иногда приходит в голову мысль: а почему бы данные не рандомизировать? Данные для таких тестов можно хранить в конфигах либо захардкодить в тесте. Это же хорошо, мы будем покрывать больше кейсов!

И сейчас я расскажу почему. Мой совет: не надо.

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

Например, девочке мы даём бесплатные бонусы сразу после регистрации, о чём уведомляем её специальным оверлеем. Теперь представим, что через некоторое время флоу регистрации становится разным.

Мы получаем тест, который в 50% случаев падает. В тесте нет логики закрытия оверлея, а он, в свою очередь, мешает каким-то дальнейшим действиям, прописанным в тесте. И это нормально, с этим приходится жить, постоянно лавируя между избыточной логикой «на все случаи жизни» (что заметно портит читабельность кода и усложняет его поддержку) и этой самой нестабильностью. Любой автоматизатор подтвердит, что UI-тесты по своей природе и так не отличаются стабильностью.

Мы его просто перезапустим и увидим, что он прошёл. В следующий раз при падении теста у нас может не быть времени на то, чтобы с ним разбираться. И успокоимся. Решим, что в нашем приложении всё работает как надо и дело в нестабильном тесте.

Что, если этот оверлей сломается? Теперь пойдём дальше. Тест продолжит проходить в 50% случаев, что существенно отдаляет нахождение проблемы.

Но бывает и по-другому. И это хорошо, когда из-за рандомизации данных мы создаем ситуацию «50 на 50». Мы пишем код, который придумывает нам случайный пароль не короче трёх символов ( иногда символов три, а иногда и больше). Например, раньше при регистрации приемлемым считался пароль не короче трёх символов. Какую вероятность падения мы получим в этом случае? А потом правило меняется — и пароль должен содержать уже не менее четырёх символов. И, если наш тест будет ловить настоящий баг, как быстро мы в этом разберёмся?

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

Как я писал выше, рандомизировать данные — плохо. Вывод. Прохождение тестов станет занимать больше времени, но с этим можно бороться. Лучше покрыть больше кейсов за счёт дата-провайдеров, не забывая про классы эквивалентности, само собой. Зато мы будем уверены, что, если проблема есть, она будет обнаружена.

Атомарность тестов (часть 1)

Давайте разберём следующий пример. Мы пишем тест, который проверяет счётчик пользователей в футере.

Сценарий простой:

  • Открыть приложение
  • Найти счётчик на футере
  • Убедиться, что он видимый

Такой тест мы называем testFooterCounter и запускаем. Потом появляется необходимость проверять, что счётчик не показывает ноль. Эту проверку мы добавляем в уже существующий тест, почему нет?

Написать новый тест или добавить в уже существующий? А вот потом появляется необходимость проверять, что в футере есть ссылка на описание проекта (ссылка «О нас»). В такой ситуации переименовать тест в testFooterCounterAndLinks кажется удачной идеей. В случае нового теста нам придётся заново поднимать приложение, готовить пользователя (если мы проверяем футер на авторизованной странице), логиниться — в общем, тратить драгоценное время.

С одной стороны, плюсы у такого подхода есть: экономия времени, хранение всех проверок какой-то части нашего приложения (в данном случае футера) в одном месте.

Если тест упадёт на первой же проверке, мы не проверим оставшуюся часть компонента. Но есть и заметный минус. Что делать? Предположим, тест упал в какой-то ветке не из-за нестабильности, а из-за бага. Тогда мы рискуем получить задачу с фиксом только этого бага, запустить тест и обнаружить, что дальше компонент тоже сломан, в другом месте. Возвращать задачу, описав только эту проблему? Пинание тикета туда-сюда в этом случае займёт много времени и будет неэффективно. И таких итераций может быть много.

В таком случае, даже имея проблему в одном кейсе, мы проверим все остальные. Вывод: стоит по возможности атомизировать проверки. И, если придётся возвращать тикет, мы сможем сразу описать все проблемные места.

Атомарность тестов (часть 2)

Рассмотрим ещё один пример. Мы пишем тест на чат, который проверяет следующую логику. Если у пользователей возникла взаимная симпатия, в чате появляется такой промоблок:

Сценарий выглядит следующим образом:

  • Проголосовать юзером А за юзера Б
  • Проголосовать юзером Б за юзера А
  • Юзером А открыть чат с юзером Б
  • Подтвердить, что блок на месте

Какое-то время тест успешно работает, но потом происходит следующее… Нет, в этот раз тест не пропускает никакой баг. 🙂

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

Писать новый кажется нецелесообразным, ведь 99% времени он будет делать то же самое, что уже существующий. Возникает тот же вопрос: написать ещё один тест или вставить проверку в уже существующий? И мы решаем добавить проверку в тест, который уже есть:

  • Проголосовать юзером А за юзера Б
  • Проголосовать юзером Б за юзера А
  • Юзером А открыть чат с юзером Б
  • Подтвердить, что блок на месте
  • Закрыть чат
  • Открыть чат
  • Подтвердить, что блок на месте

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

Например, тест называется testPromoAfterMutualAttraction. Мы откроем тест и будем пытаться вспомнить, что же он проверяет. Скорее всего, нет. Поймём ли мы, зачем в конце прописано открытие и закрытие чата? Оставим ли мы этот кусок? Особенно если этот тест писали не мы. И проверка потеряется просто потому, что её смысл будет неочевиден. Может, и да, но, если с ним будут какие-то проблемы, велика вероятность, что мы его просто удалим.

Первое: всё же сделать второй тест и назвать его testCheckBlockPresentAfterOpenAndCloseChat. Решения я тут вижу два. Второе решение — написать в коде подробный комментарий о том, зачем мы делаем эту проверку именно в этом тесте. С таким названием будет понятно, что мы не просто так совершаем какой-то набор действий, а делаем вполне осознанную проверку, поскольку был негативный опыт. В комментарии желательно также указать номер бага.

Ошибка клика по существующему элементу

Следующий пример подкинул мне bbidox, за что ему большой плюс в карму!

Предположим, у нас есть такой метод: Бывает очень интересная ситуация, когда код тестов становится уже… фреймворком.

public void clickSomeButton()

В какой-то момент с этим методом начинает происходить что-то странное: тест падает при попытке кликнуть по кнопке. Мы открываем скриншот, сделанный в момент падения теста, и видим, что на скриншоте кнопка есть и метод waitForButtonToAppear сработал успешно. Вопрос: что не так с кликом?

🙂 Самое сложное в этой ситуации то, что тест иногда может проходить успешно.

Предположим, что рассматриваемая в примере кнопка расположена на таком оверлее: Давайте разбираться.

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

Ради интереса давайте добавим дополнительный класс OLOLO для этой кнопки:

Визуально ничего не изменилось, а сама кнопка осталась на месте: После чего мы кликаем на эту кнопку.

По сути, когда JS перерисовывал нам блок, кнопку он перерисовал тоже. Что же произошло? Об этом говорит отсутствие добавленного нами класса OLOLO. Она всё ещё доступна по тому же локатору, но это уже другая кнопка.

Если за это время элемент перегенерируется, визуально это может быть незаметно, но кликнуть по нему уже не получится — метод click() упадёт с ошибкой. В коде выше мы сохраняем элемент в переменную $element.

Вариантов решения несколько:

  • Оборачивать click в try-блок и в catch пересобирать элемент
  • Добавлять кнопке какой-то атрибут, чтобы сигнализировать, что она изменилась

Текст ошибки

Напоследок простой, но не менее важный момент.

Обычно, когда пишешь тест, находишься в контексте происходящего: описываешь проверку за проверкой и понимаешь их значение. Данный пример касается не только UI-тестов, но и в них встречается очень часто. И тексты ошибок пишешь в том же контексте:

WebElement element = this.waitForPresence(By.css("a.link"), "Cannot find button");

Что может быть непонятно в этом коде? Тест ожидает появления кнопки и, если её нет, закономерно падает.

И вот у него падает тест testQuestionsOnProfile и пишет такое сообщение: “Cannot find button”. Теперь представьте, что автор теста на больничном, а за тестами присматривает его коллега. Коллеге надо как можно быстрее разобраться в происходящем, потому что скоро релиз.

Что ему придётся делать?

Следовательно, придётся внимательно изучать тест и разбираться, что же он проверяет. Открывать страницу, на которой тест упал, и проверять локатор “a.link” бессмысленно — элемента же нет.

С такой ошибкой можно сразу открывать оверлей и смотреть, куда делась кнопка. Куда проще было бы с более подробным текстом ошибки: “Cannot find the submit button on the questions overlay”.

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

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

Итог

Составление сценария теста зачастую бывает интересным занятием. Одновременно мы преследуем множество целей. Наши тесты должны:

  • покрывать как можно больше кейсов
  • работать как можно быстрее
  • быть понятными
  • просто расширяться
  • легко поддерживаться
  • заказывать пиццу
  • и так далее…

Особенно интересно работать с тестами в постоянно развивающемся и меняющемся проекте, где их приходится постоянно актуализировать: что-то добавлять и что-то выпиливать. Вот почему стоит заранее продумывать некоторые моменты и не всегда спешить с решениями. 🙂

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть