[Из песочницы] Расширяем Selenium WebDriver. Пишем робота для RSDN, не думая о контексте
Сегодня я хотел бы рассказать, как можно сделать свой PageObject паттерн на основе Selenium. Да, я знаю, что у них есть свой PageObject, но какой же программист не хочет написать свой велосипед с блэкджеком и женщинами легкого поведения.Вообще, писать автоматические тесты для UI очень сложно — постоянные проблемы, то там что-то не подгрузилось, то там запрос не дошел и упал по таймауту. Кто написал хотя бы сотню тестов — тот меня поймет. А теперь представьте, что ваши страницы не просто состоят из простого HTML, но и содержат много разных фреймов и попап окошек. Если вы хорошо знаете Selenium, то понимаете, чем это грозит. Selenium может одновременно работать только в контексте одного документа, будь то frame, iframe или отдельное модальное окно.Однажды я получил задачу написать автоматические тесты для подобного проекта, в котором очень много javascript-а, все генирируется динамически, очень много iframe-ов и ajax-запросов. Изучив Selenium, я принялся писать тесты. После третьего десятка тестов я понял, что это совсем не просто, как думал сначала. Лепить постоянные SwitchTo () в коде тестов было уже просто невозможно и код превращался в сплошные макароны. Логика теста полностью терялась за постоянными сменами контекста. В общем, я решили написать небольшой фреймворк для автоматического переключение контекста при работе с разными frame-ами.
Весь код написан на C#, с использованием NUnit, Autofac и конечно же Selenium.
Допустим, нам необходимо протестировать какие-то действия авторизованного пользователя на сайте, а форма логина находится в iframe. Как будет выглядеть такой тест:
[Test] public void SimpleTest () { var driver = new FirefoxDriver (); // Заходим в свой аккаунт driver.SwitchTo ().Frame («frmName»); driver.FindElement (By.CssSelector («input.login»)).SendKeys («my_login»); driver.FindElement (By.CssSelector («input.pass»)).SendKeys («my_pass»); driver.SwitchTo ().DefaultContent ();
// Выполняем действия на сайте // …
// Выходим из аккаунта driver.SwitchTo ().Frame («frmName»); driver.FindElement (By.CssSelector («a.logout»)).Click (); driver.SwitchTo ().DefaultContent ();
// Выполняем проверки. // …
driver.Close ();
}
Тягать постоянные SwitchTo () в каждом тесте — лень, поэтому я, как истинный ленивый программист, добился того, чтобы наши тесты выглядели следующим образом:
// Инициализируем элементы страницы
var page = _factory.CreatePage
// Логинимся на сайт page.Header.Login («my_login», «my_pass»);
// Выполняем проверки На мой взгляд, выгода очевидна, отделяем зерна от плевел и получаем чистые и самое главное читабельные тесты, которые поймет среднестатистический программист.
Давайте разберемся, что-же скрывается за этой простотой.
Структура проекта Да, кода в итоге получилось не так много. Главным здесь является класс DomContainer — это будет базовый класс для элементов на странице объединенных логически. На мой взгляд, сайт rsdn.ru является очень хорошим примером, на котором легко можно продемонстрировать все преимущества нашего фреймворка. Как будет выглядеть PageObject для сайта RSDN: public interface IRsdnPage: IDomContainer { IRsdnMenuFrame Menu { get; } IRsdnHeaderFrame Header { get; } IRsdnContentFrame Content { get; } }
public class RsdnPage: DomContainer, IRsdnPage { public IRsdnMenuFrame Menu { get; private set; } public IRsdnHeaderFrame Header { get; private set; } public IRsdnContentFrame Content { get; private set; }
public RsdnPage (IComponentContext context) : base (context) { }
// Инициализация объектов на странице protected override void Init () { base.Init ();
Header = _factory.CreateIframeItem
public interface IRsdnHeaderFrameLoggedOutState: IDomContainer { IWebElementWrapper UserName { get; } IWebElementWrapper Password { get; } IWebElementWrapper LoginButton { get; } void Login (string login, string password); } Здесь ключевым является интерфейс IWebElementWrapper, который повторяет интерфейс IWebElement и добавляет немного той самой магии, которая заменяет контекст автоматически. Необходимым условием является то, что наружу должны быть выставлены только IWebElementWrapper, а не IWebElement, чтобы весь механизм отработал корректно.Сам механизм работает очень просто и основан на интерфейсе IInterceptor включенного в сборку Castle.DynamicProxy. С помощью Autofac мы регистрируем тип следующим образом:
builder.RegisterType
// Логинимся на сайт page.Header.Login (»***»,»***»);
// Переходим на страницу поиска page.Header.GoToSearch (); page.Content.Reload ();
// Выполняем поиск на сайте по слову Selenium page.Content.Search («Selenium»);
// Переходим на страницу форумов page.Menu.GoToForums (); } На мой взгляд, просто и лаконично. Вы можете создавать своего рода DSL используя Fluent интерфейс для гибкости, а потом использовать существующие куски в других тестах.В доказательство того, что фреймворк работает на крупных проектах, показываю скриншот нашего Continuous билда:
Здесь 310 тестов.
Для тех, кому интересно посмотреть, как оно устроено внутри, я выложил проект на гитхабе.
Если возникли какие-то вопросы, задавайте их в комментариях, постараюсь ответить.