Pull to refresh

Расширяем Selenium WebDriver. Пишем робота для RSDN, не думая о контексте

Reading time 5 min
Views 8.1K
Сегодня я хотел бы рассказать, как можно сделать свой 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 тестов.

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

Если возникли какие-то вопросы, задавайте их в комментариях, постараюсь ответить.
Tags:
Hubs:
+13
Comments 0
Comments Leave a comment

Articles