Сегодня я хотел бы рассказать, как можно сделать свой PageObject паттерн на основе Selenium. Да, я знаю, что у них есть свой PageObject, но какой же программист не хочет написать свой велосипед с блэкджеком и женщинами легкого поведения.
Вообще, писать автоматические тесты для UI очень сложно — постоянные проблемы, то там что-то не подгрузилось, то там запрос не дошел и упал по таймауту. Кто написал хотя бы сотню тестов — тот меня поймет. А теперь представьте, что ваши страницы не просто состоят из простого HTML, но и содержат много разных фреймов и попап окошек. Если вы хорошо знаете Selenium, то понимаете, чем это грозит. Selenium может одновременно работать только в контексте одного документа, будь то frame, iframe или отдельное модальное окно.
Однажды я получил задачу написать автоматические тесты для подобного проекта, в котором очень много javascript-а, все генирируется динамически, очень много iframe-ов и ajax-запросов. Изучив Selenium, я принялся писать тесты. После третьего десятка тестов я понял, что это совсем не просто, как думал сначала. Лепить постоянные SwitchTo() в коде тестов было уже просто невозможно и код превращался в сплошные макароны. Логика теста полностью терялась за постоянными сменами контекста. В общем, я решили написать небольшой фреймворк для автоматического переключение контекста при работе с разными frame-ами.
Весь код написан на C#, с использованием NUnit, Autofac и конечно же Selenium.
Допустим, нам необходимо протестировать какие-то действия авторизованного пользователя на сайте, а форма логина находится в iframe. Как будет выглядеть такой тест:
Тягать постоянные SwitchTo() в каждом тесте — лень, поэтому я, как истинный ленивый программист, добился того, чтобы наши тесты выглядели следующим образом:
На мой взгляд, выгода очевидна, отделяем зерна от плевел и получаем чистые и самое главное читабельные тесты, которые поймет среднестатистический программист.
Давайте разберемся, что-же скрывается за этой простотой.
Да, кода в итоге получилось не так много. Главным здесь является класс DomContainer — это будет базовый класс для элементов на странице объединенных логически. На мой взгляд, сайт rsdn.ru является очень хорошим примером, на котором легко можно продемонстрировать ��се преимущества нашего фреймворка. Как будет выглядеть PageObject для сайта RSDN:
Мы создаем структуру страницы таким образом, чтобы потом не думать о том, какой элемент в каком frame-е находится. Указав однажды _factory.CreateIframeItem(), потом, обращаясь к любому элементу это объекта, фреймворк будет автоматически переключать конктекст драйвера в нужный frame.
Теперь рассмотрим, как будет выглядеть интерфейс логин формы для сайта RSDN:
Здесь ключевым является интерфейс IWebElementWrapper, который повторяет интерфейс IWebElement и добавляет немного той самой магии, которая заменяет контекст автоматически. Необходимым условием является то, что наружу должны быть выставлены только IWebElementWrapper, а не IWebElement, чтобы весь механизм отработал корректно.
Сам механизм работает очень просто и основан на интерфейсе IInterceptor включенного в сборку Castle.DynamicProxy. С помощью Autofac мы регистрируем тип следующим образом:
Таким образом при такой схеме вызова:
page.Header.LoggedOutState.LoginButton.Click();
Наш интерсептор перехватит вызов Click(), пройдет вверх по дереву объектов до Header, заменит контекст на нужный frame, выполнит клик по элементу в контексте этого frame-a и вернет контекст обратно. Но в коде теста мы этого не увидим, так как это происходит автоматически. Наша цель достигнута и теперь все, что нам необходимо сделать — это написать обертки под все необходимые элементы страницы и использовать их в своих тестах, не думая, в каком контексте мы находимся.
Теперь приведу пример теста для сайта RSDN, которые делает следующие действия:
На мой взгляд, просто и лаконично. Вы можете создавать своего рода DSL используя Fluent интерфейс для гибкости, а потом использовать существующие куски в других тестах.
В доказательство того, что фреймворк работает на крупных проектах, показываю скриншот нашего Continuous билда:

Здесь 310 тестов.
Для тех, кому интересно посмотреть, как оно устроено внутри, я выложил проект на гитхабе.
Если возникли какие-то вопросы, задавайте их в комментариях, постараюсь ответить.
Вообще, писать автоматические тесты для 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<IVacuumPage>(_driver);
// Логинимся на сайт
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<IRsdnHeaderFrame>(this, Driver.FindElement(By.CssSelector("frame[name='frmTop']")));
Menu = _factory.CreateIframeItem<IRsdnMenuFrame>(this, Driver.FindElement(By.CssSelector("frame[name='frmTree']")));
Content = _factory.CreateIframeItem<IRsdnContentFrame>(this, Driver.FindElement(By.CssSelector("frame[name='frmMain']")));
}
}
Мы создаем структуру страницы таким образом, чтобы потом не думать о том, какой элемент в каком frame-е находится. Указав однажды _factory.CreateIframeItem(), потом, обращаясь к любому элементу это объекта, фреймворк будет автоматически переключать конктекст драйвера в нужный frame.
Теперь рассмотрим, как будет выглядеть интерфейс логин формы для сайта RSDN:
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<WebElementWrapper>().As<IWebElementWrapper>()
.EnableInterfaceInterceptors().InterceptedBy(typeof(ContextInterceptor));
Таким образом при такой схеме вызова:
page.Header.LoggedOutState.LoginButton.Click();
Наш интерсептор перехватит вызов Click(), пройдет вверх по дереву объектов до Header, заменит контекст на нужный frame, выполнит клик по элементу в контексте этого frame-a и вернет контекст обратно. Но в коде теста мы этого не увидим, так как это происходит автоматически. Наша цель достигнута и теперь все, что нам необходимо сделать — это написать обертки под все необходимые элементы страницы и использовать их в своих тестах, не думая, в каком контексте мы находимся.
Обертки для сайта RSDN

Теперь приведу пример теста для сайта RSDN, которые делает следующие действия:
- Открывает главную страницу
- Входим в учетную запись
- Переходим на страницу поиска
- Выполняем поиск на сайте
- Переходим на страницу форумов
[Test]
public void FirstTest()
{
// Инициализируем элементы страницы
var page = _factory.CreatePage<IRsdnPage>(_driver);
// Логинимся на сайт
page.Header.Login("***", "***");
// Переходим на страницу поиска
page.Header.GoToSearch();
page.Content.Reload();
// Выполняем поиск на сайте по слову Selenium
page.Content.Search("Selenium");
// Переходим на страницу форумов
page.Menu.GoToForums();
}
На мой взгляд, просто и лаконично. Вы можете создавать своего рода DSL используя Fluent интерфейс для гибкости, а потом использовать существующие куски в других тестах.
В доказательство того, что фреймворк работает на крупных проектах, показываю скриншот нашего Continuous билда:

Здесь 310 тестов.
Для тех, кому интересно посмотреть, как оно устроено внутри, я выложил проект на гитхабе.
Если возникли какие-то вопросы, задавайте их в комментариях, постараюсь ответить.