Сегодня я хотел бы рассказать, как можно сделать свой 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<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, которые делает следующие действия:
  1. Открывает главную страницу
  2. Входим в учетную запись
  3. Переходим на страницу поиска
  4. Выполняем поиск на сайте
  5. Переходим на страницу форумов

[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 тестов.

Для тех, кому интересно посмотреть, как оно устроено внутри, я выложил проект на гитхабе.

Если возникли какие-то вопросы, задавайте их в комментариях, постараюсь ответить.