Веб-решения стали популярными в последние несколько лет, потому что они обеспечивают простой доступ для пользователей по всему миру. Они нравятся пользователям и за удобство. Пользователям не нужно устанавливать отдельное приложение; с помощью браузеров они могут подключаться к своим учетным записям с любого устройства, соединенного с Интернетом. Однако для разработчиков и тестеров тот факт, что пользователь может выбрать любой веб-браузер, создает проблему: решение приходится тестировать со множеством браузеров. В этой статье я продемонстрирую простой способ решения этой задачи, создав на C# кодированные UI-тесты, способные выполняться в любом современном браузере.
Новая Visual Studio
Несколько лет назад, когда была выпущена Visual Studio 2010, одной из ее самых интересных особенностей была возможность тестировать UI веб-решений. Однако в то время существовали некоторые ограничения на использование этой технологии; например, единственным поддерживаемым веб-браузером был Internet Explorer. Более того, тестирование UI опиралось на запись действий пользователя на веб-сайте и последующее их воспроизведение для имитации реальных операций, что многие разработчики находили неприемлемым.
Новая Visual Studio 2013, доступная в виде кандидата на выпуск (RC), вносит массу усовершенствований и новшеств во многих областях — от новых средств IDE до расширенной инфраструктуры тестирования (длинный список изменений в RC-версии см. по ссылке bit.ly/1bBryTZ). С моей точки зрения, особый интерес представляют два новых средства. Во-первых, теперь можно тестировать UI не только в Internet Explorer (включая Internet Explorer 11), но и во всех остальных современных браузерах, таких как Google Chrome и Mozilla Firefox. Во-вторых, появилось то, что с точки зрения разработки тестов даже важнее, а именно (как говорят в Microsoft): «конфигурируемые свойства для кодированных тестов UI в браузере». По сути, эта новая функциональность определяет набор критериев поиска для UI-элементов. Я опишу эти средства подробнее далее в этой статье.
Тестируемая система
На основе этих двух новых средств я создам межбраузерные, полностью кодированные тесты UI (coded UI tests). Для своей тестируемой системы (system under test, SUT) я хочу использовать общедоступное, хорошо известное веб-приложение, поэтому я выбрал Facebook. Мне нужно охватить два основных пользовательских сценария. Первый — позитивный (чистый) тестовый сценарий (positive test case), который будет показывать страницу профиля после успешного входа. Второй — негативный (грязный) тестовый сценарий (negative test case), в котором я ввожу неправильные удостоверения пользователя и пытаюсь войти. В этом случае я ожидаю какое-либо сообщение об ошибке в ответе пользователю.
Мне придется решить несколько проблем. Во-первых, нужно запускать правильный браузер (на основе конфигурации теста), и он должен предоставлять доступ по конкретному URL. Во-вторых, в период выполнения от HTML-документа должен быть получен определенный элемент управления, чтобы обеспечить ввод для имитируемого пользователя. Везде, где нужно, в этот элемент управления нужно вводить значения и щелкать правильные кнопки для передачи HTML-формы серверу. Код также должен быть способен обрабатывать ответ от сервера, проверять его и, наконец, закрывать браузер, когда тестовые сценарии заканчиваются (в методе очистки теста).
Перед кодированием
Прежде чем начать кодировать, необходимо подготовить среду, что делается довольно просто. Первым делом скачайте Visual Studio 2013 RC по ссылке bit.ly/137Sg3U. По умолчанию Visual Studio 2013 RC позволяет создавать кодированные тесты UI только для Internet Explorer, но это не то, в чем я заинтересован; я хочу создать тесты для всех современных браузеров. Конечно, никаких ошибок компиляции не будет, пока я указываю в своем коде, что тесты должны запускаться применительно к браузерам, отличным от Internet Explorer, но в период выполнения будет сгенерировано необрабатываемое исключение. Немного позже я покажу и то, как сменять браузер. Чтобы избежать проблем при кодировании, нужно скачать и установить расширение Visual Studio под названием «Selenium components for Coded UI Cross-Browser Testing» (bit.ly/Sd7Pgw), которое позволит выполнять тесты в любом браузере, установленном на компьютере.
Прыгаем в код
Теперь я могу продемонстрировать, как создать новый проект Coded UI. Откройте Visual Studio 2013 и выберите File | New Project | Templates | Visual C# | Tests | Coded UI Test Project. Введите имя проекта и щелкните OK, чтобы увидеть новое решение, как показано на рис. 1.
Рис. 1. Создание нового Coded UI Test Project
Решение, по сути, делится на три тесно связанные группы (рис. 2). Первая группа содержит пространство имен Pages, куда входит класс BasePage, от которого наследуются ProfilePage и LoginPage. Эти классы предоставляют свойства и логику для операций над страницей, отображаемой в данный момент в браузере. Такой подход помогает отделить реализации тестовых сценариев от специфичных для браузера операций, таких как поиск элемента управления по Id. Тестовые сценарии напрямую манипулируют свойствами и функциями, предоставляемыми классами страницы.
Рис. 2. Схема решения Coded UI Test
Во вторую группу я помещаю все расширения (UIControlExtensions), селекторы (SearchSelector) и общие для браузеров классы (BrowserFactory, Browser). Это подмножество объектов содержит логику реализации для механизма поиска HTML-элементов (об этом — позже). Кроме того, я добавил собственные объекты, относящиеся к браузеру, — они помогают в выполнении тестовых сценариев с использованием правильного веб-браузера. Последняя группа включает файл теста (FacebookUITests) с реализациями тестовых сценариев. Эти тестовые сценарии никогда не работают с браузером напрямую; вместо этого они используют классы панелей (panel classes).
Важная часть моего проекта — механизм поиска HTML-элементов управления, поэтому первым делом я создаю статический класс UIControlExtensions, содержащий логику реализации для поиска и получения определенных элементов управления от текущей открытой в браузере страницы. Чтобы облегчить кодирование и получить возможность повторного использования, мне нужно избавиться от инициализации экземпляров этого классов при каждом его использовании. Для этого я намерен реализовать его как метод расширения, который будет расширять встроенный тип UITestControl. Кроме того, все реализуемые мной функции расширения будут обобщенными. Они должны наследоваться от HtmlControl (базового класса для всех элементов управления UI в Coded UI Test Framework) и содержать конструктор по умолчанию без параметров. Я хочу, чтобы эта функция была обобщенной потому, что собираюсь искать только определенные типы элементов управления (список доступных HTML-типов см. по ссылке bit.ly/1aiB5eW).
Я буду передавать критерии поиска в свою функцию через параметр selectorDefinition типа SearchSelector. Класс SearchSelector является простым, но очень полезным типом. Он предоставляет ряд свойств, таких как ID или Class, которые можно устанавливать из другой функции и впоследствии преобразовывать, используя отражение, в класс PropertyExpressionCollection (bit.ly/18lvmnd). Потом этот набор свойств можно применять как фильтр для извлечения только небольшого подмножества HTML-элементов управления, которые подходят указанным критериям. В дальнейшем сгенерированный набор свойств назначается свойству SearchProperties (bit.ly/16C20iS) обобщенного объекта, который позволяет вызывать свойство Exists и функцию FindMatchingControls. Учтите, что алгоритмы Coded UI Test Framework по умолчанию не будут искать указанные элементы управления во всей странице и будут обрабатывать все дочерние и «внучатые» элементы только в расширенном UITestControl. Это помогает увеличить производительность тестовых сценариев, поскольку критерии поиска можно применять лишь к небольшому подмножеству HTML-документа, например ко всем дочерним элементам некоего DIV-контейнера, как показано на рис. 3.
Рис. 3. Поиск HTML-элементов управления
private static ReadOnlyCollection<T>
FindAll<T>(this UITestControl control,
SearchSelector selectorDefinition) where T : HtmlControl, new()
{
var result = new List<T>();
T selectorElement = new T { Container = control };
selectorElement.SearchProperties.AddRange(
selectorDefinition.ToPropertyCollection());
if (!selectorElement.Exists)
{
Trace.WriteLine(string.Format(
"Html {0} element doesn't exist for given selector {1}.",
typeof(T).Name, selectorDefinition),"UI CodedTest");
return result.AsReadOnly();
}
return selectorElement
.FindMatchingControls()
.Select(c => (T)c).ToList().AsReadOnly();
}
}
Я реализовал основную часть механизма поиска, но функция FindAll<T> требует уймы кода и знания того, как добиться ее правильной работы, — вы должны указывать параметры поиска, проверять, существует ли элемент, и т. д. Вот почему я решил сделать ее закрытой и предоставлять вместо нее две другие функции:
public static T FindById<T>(this UITestControl control,
string controlId) where T : HtmlControl, new()
public static T FindFirstByCssClass<T>(this UITestControl control,
string className, bool contains = true) where T : HtmlControl, new()
Эти обобщенные методы гораздо полезнее, так как каждый из них выполняет по одной задаче и тем самым сокращает список ввода в простые типы. За кулисами обе функции вызывают функцию FindAll<T> и оперируют ее результатом, но эта реализация скрыта.
Работа с любым браузером
Я уже проделал какую-то часть работы по поиску и получению элементов управления, но, чтобы протестировать правильность реализации функций, мне нужно запустить веб-браузер. Запуск конкретного браузера так же прост, как и любая другая операция, относящаяся к браузеру. Я уже упоминал, что хочу поместить все связанные с браузерами операции в классы, относящиеся к странице. Однако запуск браузера не является частью тестирования — это обязательное требование. Исходя из рекомендаций, я решил создать класс BasePage (рис. 4), содержащий общие операции для всех производных классов страницы (включая запуск браузера), чтобы избежать любой избыточности.
Рис. 4. Класс BasePage
public abstract class BasePage : UITestControl
{
protected const string BaseURL = "https://www.facebook.com/";
/// <summary>
/// Получаем URL текущей страницы
/// </summary>
public Uri PageUrl{get; protected set;}
/// <summary>
/// Сохраняем корневой элемент управления для страницы
/// </summary>
protected UITestControl Body;
/// <summary>
/// Получаем окно текущего браузера
/// </summary>
protected BrowserWindow BrowserWindow { get; set; }
/// <summary>
/// Конструктор по умолчанию
/// </summary>
public BasePage()
{
this.ConstructUrl();
}
/// <summary>
/// Формируем URL производной страницы на основе BaseURL
/// и URL конкретной страницы
/// </summary>
/// <returns>A specific URL for the page.</returns>
protected abstract Uri ConstructUrl();
/// <summary>
/// Проверяем правильно отображения производной страницы
/// </summary>
/// <returns>True if validation conditions passed.</returns>
public abstract bool IsValidPageDisplayed();
}
Статическая обобщенная функция Launch<T> также является частью класса BasePage. В теле этой функции инициализируется новый экземпляр типа конкретной страницы (производного от BasePage) с помощью конструктора по умолчанию без параметров. Далее в коде задается целевой веб-браузер на основе значения параметра browser («ie» для Internet Explorer, «chrome» для Google Chrome и т. д.). Этот параметр указывает браузер, в котором будет выполняться текущий тест. Следующий шаг — переход по какому-то URL в выбранном браузере. Эта операция обрабатывается в BrowserWindow.Launch(page.ConstructUrl()), где ConstructUrl — конкретная функция для каждой производной страницы. После запуска окна браузера и перехода по конкретному URL я сохраняю HTML body в свойстве BasePage и развертываю окно браузера (это не обязательно, но желательно, поскольку элементы управления страницы могут перекрываться и автоматизированные UI-операции могут завершиться неудачей). Затем я очищаю файлы cookie, так как каждый тест должен быть независимым. Наконец, в функции Launch (рис. 5) я проверяю, является ли правильной текущая отображаемая страница, поэтому вызываю IsValidPageDisplayed, которая выполняется в контексте обобщенной страницы. Эта функция находит все требуемые HTML-элементы управления (login, password, submit) и проверяет, существуют ли они в странице.
Рис. 5. Функция Launch
public static T Launch<T>(
Browser browser = Browser.IE,
bool clearCookies = true,
bool maximized = true)
where T : BasePage, new()
{
T page = new T();
var url = page.PageUrl;
if (url == null)
{
throw new InvalidOperationException("Unable to find URL for requested page.");
}
var pathToBrowserExe = FacebookCodedUITestProject
.BrowserFactory.GetBrowserExePath(browser);
// Задаем текущий браузер для теста
BrowserWindow.CurrentBrowser = GetBrowser(browser);
var window = BrowserWindow.Launch(page.ConstructUrl());
page.BrowserWindow = window;
if (window == null)
{
var errorMessage = string.Format(
"Unable to run browser under the path: {0}", pathToBrowserExe);
throw new InvalidOperationException(errorMessage);
}
page.Body = (window.CurrentDocumentWindow.GetChildren()[0] as
UITestControl) as HtmlControl;
if (clearCookies)
{
BrowserWindow.ClearCookies();
}
window.Maximized = maximized;
if (!page.IsValidPageDisplayed())
{
throw new InvalidOperationException("Invalid page is displayed.");
}
return page;
}
Веб-браузеры постоянно развиваются, и вы можете упустить момент, когда что-то меняется. Иногда это означает, что в новой версии браузера некоторые средства больше не доступны, что в свою очередь приводит к провалу каких-то тестов, даже если ранее они проходили. Вот почему важно отключать автоматические обновления браузера и ждать, пока в компонентах Selenium для Coded UI Cross-Browser Testing не появится поддержка новой версии. Иначе в период выполнения могут возникать неожиданные исключения, как показано на рис. 6.
Рис. 6. Исключение после обновления веб-браузера
Тестирование, тестирование и еще раз тестирование
Наконец, я напишу логику для тестов. Как упоминалось, мне нужно тестировать два основных пользовательских сценария. Первый — это положительный процесс входа (positive login process) (второй, отрицательный тестовый сценарий доступен в исходном коде проекта, который можно скачать по ссылке archive.msdn.microsoft.com/mag201312Testing). Чтобы выполнить этот тест, я должен создать класс конкретной страницы, производный от BasePage (рис. 7). В закрытые поля нового класса я помещаю все значения констант (имена элементов управления, идентификаторов и CSS-класса) и создаю выделенные методы, использующие эти константы для получения определенных UI-элементов от текущей страницы. Кроме того, я создаю функцию TypeCredentialAndClickLogin(string login, string password), которая полностью инкапсулирует операцию входа (login operation). В период выполнения она находит все необходимые элементы управления, имитирует ввод значений, передаваемых как параметры, а затем «нажимает» кнопку Login, инициируя событие щелчка левой кнопки мыши.
Рис. 7. Страница Login
public class LoginPage : BasePage
{
private const string LoginButtonId = "u_0_1";
private const string LoginTextBoxId = "email";
private const string PasswordTextBoxId = "pass";
private const string LoginFormId = "loginform";
private const string ErrorMessageDivClass = "login_error_box";
private const string Page = "login.php";
/// <summary>
/// Формируем URL для страницы
/// </summary>
/// <returns>Uri of the specific page.</returns>
protected override Uri ConstructUrl()
{
this.PageUrl = new Uri(string.Format("{0}/{1}", BasePage.BaseURL,
LoginPage.Page));
return this.PageUrl;
}
/// <summary>
/// Проверяем, правильная ли страница отображается
/// </summary>
public override bool IsValidPageDisplayed()
{
return this.Body.FindById<HtmlDiv>(LoginTextBoxId) != null;
}
/// <summary>
/// Получаем кнопку Login от страницы
/// </summary>
public HtmlInputButton LoginButton
{
get
{
return this.Body.FindById<HtmlInputButton>(LoginButtonId);
}
}
/// <summary>
/// Получаем текстовое поле входа от страницы
/// </summary>
public HtmlEdit LoginTextBox
{
get
{
return this.Body.FindById<HtmlEdit>(LoginTextBoxId);
}
}
/// <summary>
/// Получаем текстовое поле пароля от страницы
/// </summary>
public HtmlEdit PasswordTextBox
{
get
{
return this.Body.FindById<HtmlEdit>(PasswordTextBoxId);
}
}
/// <summary>
/// Получаем диалоговое окно ошибки, если войти не удалось
/// </summary>
public HtmlControl ErrorDialog
{
get
{
return this.Body.FindFirstByCssClass<HtmlControl>("*login_error_box ");
}
}
/// <summary>
/// Вставляем логин и пароль в поля ввода
/// и щелкаем кнопку Login
/// </summary>
public void TypeCredentialAndClickLogin(string login, string password)
{
var loginButton = this.LoginButton;
var emailInput = this.LoginTextBox;
var passwordInput = this.PasswordTextBox;
emailInput.TypeText(login);
passwordInput.TypeText(password);
Mouse.Click(loginButton, System.Windows.Forms.MouseButtons.Left);
}
}
После создания необходимых компонентов можно создать тестовый сценарий (test case). Эта функция теста будет проверять успешное выполнение операции входа. В начале тестового сценария я запускаю страницу Login с помощью статической функции Launch<T>. Все требуемые значения я передаю в поля ввода логина и пароля, а затем имитирую щелчок кнопки Login. По окончании операции я проверяю, является ли новая отображаемая панель страницей Profile:
[TestMethod]
public void FacebookValidLogin()
{
var loginPage = BasePage.Launch<LoginPage>();
loginPage.TypeCredentialAndClickLogin(fbLogin, fbPassword);
var profilePage = loginPage.InitializePage<ProfilePage>();
Assert.IsTrue(profilePage.IsValidPageDisplayed(),
"Profile page is not displayed.");
}
В процессе поиска элемента управления с определенным CSS-классом я заметил, что в Coded UI Test Framework может возникнуть проблема. В HTML элементы управления могут иметь более одного имени класса в атрибуте class, и это, конечно же, влияет на инфраструктуру, с которой я работаю. Если, например, текущий веб-сайт содержит элемент DIV с атрибутом class «A B C» и для поиска всех элементов управления с CSS-классом «B» используется свойство SearchSelector.Class, то я могу не получить никакого результата — ведь «A B C» не тождественно «B». Чтобы устранить эту проблему, я ввел нотацию звездочки «*», которая меняет ожидания класса с «equals» на «contains». Таким образом, чтобы этот пример работал, нужно сменить класс «B» на «*B».
Что будет, если…
Иногда тесты проваливаются, и вы задаетесь вопросом — почему. Во многих случаях ответ на этот вопрос дает анализ журнала теста. Но не всегда. В Coded UI Test Framework новое средство предоставляет по запросу дополнительную информацию.
Допустим, тест провалился потому, что отображаемая страница отличается от ожидаемой. В журналах я вижу, что некоторые запрошенные элементы управления не найдены. Это хорошая информация, но она не дает полного ответа. Однако с помощью нового средства я могу получить снимок текущего экрана. Чтобы задействовать это средство, нужно просто добавить его и запрограммировать способ его сохранения в методе очистки теста, как показано на рис. 8. Теперь я получу подробную информацию о том, почему провалился тест.
Рис. 8. Метод очистки теста
[TestCleanup]
public void TestCleanup()
{
if (this.TestContext.CurrentTestOutcome != null &&
this.TestContext.CurrentTestOutcome.ToString() == "Failed")
{
try
{
var img = BrowserWindow.Desktop.CaptureImage();
var pathToSave = System.IO.Path.Combine(
this.TestContext.TestResultsDirectory,
string.Format("{0}.jpg", this.TestContext.TestName));
var bitmap = new Bitmap(img);
bitmap.Save(pathToSave);
}
catch
{
this.TestContext.WriteLine("Unable to capture or save screen.");
}
}
}
Заключение
В этой статье я показал, насколько легко и быстро приступить к использованию новой инфраструктуры Coded UI Test Framework в Visual Studio 2013 RC. Конечно, я описал лишь базовое применение этой технологии, включая управление разными браузерами и поддержку множества операций поиска, получения и манипулирования HTML-элементами управления. Однако функциональность этой инфраструктуры гораздо шире, и ее стоит исследовать.