Слои фреймворка для тестирования Web UI

Всем привет!

Предлагаю немного поговорить об архитектуре фреймворков, которыми вы пользуетесь/разрабатываете. Чтобы не говорить обо всем сразу, предлагаю сузить круг до фреймворков, использующих WebDriver, а вопрос - до слоев фреймворка.
Напишите какие слои выделены в вашем фреймворке и почему, с кратким обоснованием?

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

Цели разбиения на слои две:

  1. разделение ответственности
  2. максимальное переиспользование кода

Выделены следующие слои:

  1. тесты. Это непосредственно сами тесты, которые используют API, предоставляемый нижележащим сервисным (как я его называю) слоем
  2. сервисы. Это очень важный слой. Он представляет собой классы, которые предоставляют тестам методы взаимодействия с тестируемым приложением в терминах бизнес-функций. Например, CientService предоставляет методы createClient(), findClientInList() и т.п. То есть тесты не работают (обычно) с отдельными страницами, а используют методы сервисного слоя, который скрывает от теста навигацию между страницами и взаимодействие со страницами. Это позволяет упростить код теста, сделать его keyword-driven, так что тест читается в виде удобном для человека
  3. страницы. Классы этого слоя - это Page Objects. Каждый класс этого слоя предоставляет средства взаимодействия с элементами страницы. Методы доступны вышележащим слоям: сервисному и тестам. Но тесты без необходимости напрямую со страницами не работают
  4. элементы страницы. Классы этого слоя предоставляют методы работы с отдельными элементами страниц. Лично у меня не для каждого тестируемого приложения получается сделать этот слой полезным. Если верстка одинаковых элементов интерфейса различна на разных страницах, то пользы от этого слоя не будет.

Использование слоев:

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

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

  1. Тест продолжает использовать метод ClientService.createClient(). Тогда из страничного слоя, со страницы создания клиента, при попытке засабмитить форму с незаполненным обязательным полем, должен выбрасываться Exception, который тест должен перехватить и проанализировать на предмет ожидаемой реакции
  2. Тест использует из сервисного слоя только навигацию до страницы создания клиента. А дальше работает непосредственно со страницей

Интересны такие вопросы:

  1. Используете ли вы сервисный слой?
  2. Возможно используете другие принципы разбиения на слои?
  3. Возможно не используете разбиение на слои вовсе?
  4. Если используете слои, то как решаете задачу с failure-тестами (как убеждаетесь в правильности реакции системы на тест)
4 лайка

Если смотреть с самого верха, то элементов фреймворка у меня 4:

  1. Ядро (Framework Core) – отвечает за все, что связано с тестами, но там нет ничего, что связано с бизнес-логикой работы приложения: модуль отчетности, методы инициализации вебдрайвера, расширения для вебдрайвера, базовые классы для страниц и тестов. И – глобальную конфигурацию (т.е. чтение данных из глобального файла конфигурации, а также некоторые глобальные свойства в коде)
  2. Database – обеспечивают выборки из базы данных. У нас есть несколько проектов, но они работают с одной базой данных. По сути, этот модуль обеспечивает доступ к этой базе.
    Здесь описываются методы для получения данных. Их можно вызвать как из теста так и из модели. Использовать запросы типа «select * from users» – запрещено из тестов и из тестовой модели. Обязательно должен быть создан отдельный метод в Модуле Database.
  3. Тестовая Модель (TestModel) – через нее можно производить операции с приложением. Это сборка, где создается абстракции для работы с проектом: вней хранятся PageObject-ы, Классы с данными (DTO; Data Transfer Object). И есть Бизнес-шаги, например, такие как CreateUser(username) и .т.д. Эти бизнес шаги (Steps), например, @joemast, называет – сервисами.
  4. Тесты – ну да, как же без них :). Тесты у меня взаимодействуют с приложением через TestModel и Database. У меня была мысль, сделать взаимодействие только через TestModel. Т.е. скрыть использование Database, но, потом я от этой мысли отказался, так как это порождает еще более запутанный абстракциями код.

Вместе Tests и TestModel составляют тестовый проект. Такой проект у меня пока один, но предусматривается появление следующих.

По сути, модуль Tests – это только потребитель функционала, он не предоставляет функциональность другим модулям.

Самый важный строительный блок – это PageObjects. И они создаются по достаточно жестким правилам:

  • Single Responsibility (из SOLID) – Пейджобжект отвечает только за действия над страницей, которую он описывает. Т.е. на странице нового пользователя, есть методы FillForm и Save, но не вкоем случае не (CreateUser()). В некоторых ситуациях, Педжобжект может взаимодействовать с другими Пейджобжектами и с Бизнес-шагами. Но, делается это на уровне абстракций и API. Пейджобжекты не взаимодействуют с базой данных или веб-элементами других страниц напрямую
  • Все веб-элементы – private или protected. Т.е. могут быть использованы только внутри Пейджобжекта и его наследников. Наружу предоставляются лишь методы.
    Интересно, что иногда так руки и чешутся написать в тесте: UserForm.btnSave.Click() – но, нельзя. Нужно создать метод, и поместить эту строку кода а него. Т.е. в тесте можно писать UserFrom.Save().
  • .Invoke() – в каждом Пейджобжекте есть метод, который открывает страницу по «дефолтному» сценарию. Я рассказывал об этом подходе в докладе «За пределами PageObject» c 40-й минуты.
    Т.е., чтобы открыть страницу любого уровня вложенности, нужно вызвать, UserPage.Invoke()
  • .GetExpectedControls() – возвращает «дефолтные» ожидаемые элементы на странице. Внимание, не все, а только самые важные.

И самое интересное: когда завершено создание нового Пейджобжекта, на него обязательно должен быть написан тест. Тест вызывает .Invoke() страницы, а потом, проверят наличие каждого элемента при помощи .GetExpectedControls().
Тем самым, создается очень легковесный набор тестов, который с одной стороны, тестирует все самые важные элементы пейджобжекта, а с другой стороны – является хорошим смоук-тестом для каждой страницы приложения. В момент работы тестов, автоматически идет проверка на неожиданные JavaScript-ошибки и крэши сервера (500-е ошибки), их тоже отлавливают смоук тесты.
В тестах можно использовать пейджобжекты как напрямую, так и работать с бизнес-шагами. Просто, если один и тот же код повторяется очень часто – он выносится как бизнес-шаг.

Интересны такие вопросы:

  1. Используете ли вы сервисный слой?

Да, но от варианта работы только через сервисный слой, я отказался. Это порождает необходимость писать дополнительный код для редко вызываемых действий. Для часто используемых действий, такой код пишется сразу: (User.Create(), User.Open(), User.Delete())

4.Если используете слои, то как решаете задачу с failure-тестами (как убеждаетесь в правильности реакции системы на тест)

А вот, почему я не запрещаю работать с Пейджобжектами из теста.

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

Это пример реализации теста, который напрямую вызывает методы Пейджобжекта:

var userFormPage = new UserFormPage();
var form = UserFrom.Default;
form.Password = “123”
form.ConfirmPass = “666”
userFormPage.FillForm(form)
userFormPage.VerifyConfirmValidationError(“Confirm and Password fields do not match”)
8 лайков

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

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

Дмитрий, а где тогда вот эти проверки: “В момент работы тестов, автоматически идет проверка на неожиданные JavaScript-ошибки и крэши сервера (500-е ошибки), их тоже отлавливают смоук тесты”?
В самих smoke-тестах, либо в PageObject?

Лично у меня проверка “ошибки, которая никогда не должна произойти, иначе всё пропало” лежит в ресурсах работы со страницей. Это все равно что exception в библиотеке, и все тестировщики должны как минимум видеть эту проверку.

Если бы небыло других вариантов, то и мне пришлось бы просить разработчиков добавить специальные скрипты для отлова JS ошибок. А, ошибки сервера отлавливаются самим приложением – появляется специальный MessageBox.

У меня есть один метод в тестовом фреймворке, который либо явно вызывается, либо вызывается из других часто используемых методов.
Этот метод инджектит JavaScript код для отолова ошибок в случае, если он небыл заинджектен и проверяет специальный массив внутри страницы: если размер больше нудя – то есть JS ошибки.

Этот подход описан вот в этой статье:

http://watirmelon.com/2012/12/19/using-webdriver-to-automatically-check-for-javascript-errors-on-every-page/

Есть еще один подход, которым я не пользовался – это внедрение кода при помощи прокси-сервера:

1 лайк

У меня сделано так:

  1. в методах PageObject, где возможны возникновения ошибок на соответствующей странице, производится проверка на наличие ошибок после выполнения операции с элементом. Например, при сабмите формы проверяется не появилась ли подсветка неправильно заполненных полей с сообщениями об ошибках
  2. Если ошибки появились, создается массив с перечнем ошибок, выбрасывается исключение, в которое этот массив передается
  3. это исключение можно поймать и обработать в тесте. Никакой уровень кроме тестового это исключение не должен обрабатывать

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

@joemast, а как вы решаете проблему с локализацией ошибки?, ведь, по моему предположению, такие ошибки сложно дебажить.

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

Если вопрос про верстку, то это не проверяется сейчас.

Или вопрос ещё в чём-то?

Да, вопрос был про “в каком поле и на какой странице произошла ошибка”. И ответ уже есть. Спасибо.

тут опять же сложность реализации зависит от специфики тестируемого приложения. Если при незаполненном поле текст ошибки всегда одинаков: “поле не может быть пустым”, тогда конечно массив таких ошибок в тесте будет обработать невозможно и их надо будет снабдить дополнительной информацией об имени поля.

А если текст ошибки однозначно характеризует незаполненное поле: “Не указан счет получателя”, то тут проблем не возникает и реализация такого механизма упрощается.

1 лайк

Тупой вопрос конечно:) Но, почему все таки “нельзя” ? :blush:
Понятно зачем нужны абстракции - как минимум для того что если что то поменятется то изменить только в одном месте… Но что может поменяться - если наш локатор-елемент - принадлежит только одной странице… И особенно в примере с save() - какие скелеты могут вылезти если мы не напишем отдельний метод save(), а оставим как есть, и сделаем тот же код тест более “однозначным” (в любом случае это будет не код теста а код бизнес шага скорее всего).
Да и потом… Дима, Вы же кажеться на С# пишете, разве btnSave - это не проперти? Ее же всегда можно будет изменить не изменяя кода UserForm.btnSave.Click(). Или я уже что то забыл или упустил? :slight_smile: Может на сишарпе не реализуют элементы как проперти, не знаю… Но если так, почему тогда так не делать?

Еще об однозначности…
Мне кажеться чем меньше (в меру) абстракций тем все таки проще читать код, не переходя каждый раз к реализации метода и смотря что он делает:)
В нашем случае смотря на

проще понять что делает наш шаг, чем посмотрев на

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

Кстати, вначале действительно смоук тесты на страницу с помощью такого метода могут быть полезны… Но потом ведь они все равно будут покрываться через другие тесты, которые тоже не плохо было бы держать в “смоук” сьютах… Или “getExpectedControls()” используется как пре-смоук сьют к основному смоук-сьюту ?.. (имею ввиду аксептенс сьют который бегаеться после каждого нового билда и должен быть достаточно быстрым чтобы сигнализировать о том поламался билд или нет…)
Я сначала тоже использовал “IHaveExpectedControls” но потом не нашел большой выгоды… Так как если в первую очередь писать только самые приоритетные тесты с целью создать аксептенс сьют упомянутый раннее, и добавлять в пейджобджект элементы только нужные в каждом следующем автоматизируемом сценарии - то это будут те же смоук тесты на страницу. Работы будет только немножко меньше, и время исполнения всего смоук сьюта тоже будет меньше, так как не будет дублирования - присуствие основных элементов страници будет проверяться неявно.

@Ayia, вы правы на счет меры в абстракциях. Действительно, мне время от времени самому приходится наталкивается на то, что я начинаю писать какой-то заумный код, который в итоге сложно дебажить да и вообще понять. Говорят, что автоматизаторы пишут приложения, которые тестируют другое приложение. На самом деле это не совсем так, в первую очередь, автоматизаторы пишут код, который должен быть эффективным с точки зрения работы, и четким и понятным с точки зрения поддержки.

Путем проб и ошибок, я пришел к тому, что все элементы страницы, задекларированные в PageObject – должны быть приватными. Для себя я понял, что если это правило соблюдать – то сам код потом легче поддерживать. Конечно же, получается так, что чтобы избежать проблем на нескольких страницах – это правило необходимо применять ко всем страницам. Это может быть избыточно, но лично я остановился на таком пути.

История с однострочным .Save()

На самом деле, сейчас, на нескольких страницах приложения, которое я тестирую, метод Save когда-то состоял из одной строчки, но потом был расширен.
Дело в том, что авто-тесты я писал под свою «меленькую» тестовую базу данных. На этой базе не было никакой необходимости в дополнительных ожиданиях, потому что все работало быстро.
На «больших» продакшн базах (да, их много разных), оказалось, что из-за специфической конфигурации, потребовалось добавить ожидания. В этом случае, специальный код ожиданий мне пришлось добавить в метод .Save отдельных Пейджобжектов, при этом, не модифицируя код теста.

Вторая история с методом .Save в том, что при создании некоторых сущностей кнопка сохранения называется «Add», а при редактировании этой же сущности – “Update”. И при этом – это два разных элемента страницы, просто один прячется, а второй показывается. В таком случае, метод .Save сам выбирает на какую кнопку ему нажать. А бизнес-операция одна – Save().

С точки зрения автоматизации – это закон: у всех Пейджобжектов, в которых можно сохранить сущность – должна быть операция .Save(). Если ее нет – то нужно добавить при необходимости. Это один из общих законов, который позволяет построить выстроить некоторые ожидания даже человеку, который впервые открывает класс Пейджобжекта, с которым тот не работал ранее.

По поводу смоук тестов, вероятно, мы понимаем этот сьют по-разному. Для меня, смоук тесты – это набор простых тестов, которые уже нельзя разделить на смоук и пре-смоук.
Мои смоук тесты – просто открывают каждую страницу приложения, и проверяют лишь наличие обязательных контролов.
Смоук тесты за счет своей простоты – могут быстрее сигнализировать об изменениях на странице, JavaScript ошибках, которые лежат на поверхности и таких же серверных крэшах.
Если падают смоук тесты – то уже нет смысла смотреть и запускать Acceptance-тесты.
Для смоук тестов, для нас приоритетна скорость их работы. Т.е. нужно, чтобы они завершились в течении 5 – 10-ти минут.
Для Acceptance-тестов, в моем понимании, желательно, чтобы они завершились побыстрее, но, тут для нас приоритетом является покрытие функционала приложения. Желательно, чтобы такие тесты проходили как можно раньше, но и работа сьюта в течении 2, 4-х или 8-ми часов тоже будет удовлетворительной.

Если бы acceptance-сьют проходил за 10 минут, то я бы, наверное, отказался бы от смоук тестов. Это возможно, но при отказе от UI тестов.

Мда… Все так все так…
Еще наверное много чего зависит от проекта, как всегда:)

Я же тогда поднимал тему о статистике…
Некоторые ребята упоминали о своих “аксептенс” сьютах в 150-200 тестов которые исполняються в течении 5-10 минут…

Нельзя так тесты считать штуками :D. Когда я слушу 20 -200 тестов, то мне интересно что это за тесты такие, что они делают? И что они тестируют?

И да, очень много чего зависит от проекта. И от окружения. Например, в данный момент, мой смоук сьют проходит за 5 минут под PhantomJS. А под IE9 – 15 минут.

Как по мне, аццептанс тесты должны обеспечить хотя бы 70-80% покрытия всех бизнес фич приложения. За 20 минут для меня, с этим можно справиться только распараллеливая тесты. Т.е, когда, например, 10 инстансов идут каждый по 20 минут, а в итоге, аггрегируют информацию в единый отчет