Автоматизация тестирования – место встречи двух дисциплин: разработки и тестирования. Наверное поэтому, я отношу эту практику к сложным, но интересным.
Путем проб и ошибок мы пришли к следующему технологическому стеку:
- SpecFlow (опционально): DSL
- NUnit: тестовый фреймворк
- PageObject + PageElements: UI-абстракиця
- Контекст тестирования (информация о целевом окружении, пользователях системы)
- Selenium.WebDriver
Для запуска тестов по расписанию мы используем TFS 2012 и TeamCity.
В статье я опишу, как мы к этому пришли, типовые ошибки и пути их решения.
Зачем так сложно?
Очевидно, что автоматизация тестирования имеет множество плюсов. Автоматическое решение:
- Экономит время
- Исключает человеческий фактор при тестировании
- Снимает бремя рутинного регрессионного тестирования
Все, кто хоть раз занимался автоматизированным тестированиям, знают об оборотной стороне медали. Автоматические тесты могут быть:
- Хрупкими и «ломаться» из-за изменения UI
- Непонятными, содержать код «с душком»
- Недостоверными: тестировать неверное поведение или зависеть от особенностей окружения
Для примера, рассмотрим следующий код. По названию видно, что мы тестируем то, что по запросу «gangam style» гугл выдаст YouTube-канал корейского популярного исполнителя PSY первым результатом.
[Test] public void Google_SearchGangnamStyle_PsyYouTubeChanelIsOnTop() { var wd = new OpenQA.Selenium.Firefox.FirefoxDriver {Url = "http://google.com"}; try { wd.Navigate(); wd.FindElement(By.Id("gbqfq")).SendKeys("gangnam style"); wd.FindElement(By.Id("gbqfb")).Click(); var firstResult = new WebDriverWait(wd, TimeSpan.FromSeconds(10)).Until( w => w.FindElement(By.CssSelector("h3.r>a"))); Assert.AreEqual("PSY - YouTube", firstResult.Text); Assert.AreEqual("http://www.youtube.com/user/officialpsy", firstResult.GetAttribute("href")); } finally { wd.Quit(); } }
В этом тесте очень много проблем:
- Перемешаны слои приложения (драйвер, локаторы, результаты)
- Строки зашиты в тесте
- Для того, чтобы изменить веб-драйвер, например на IE придется менять все тесты
- Локаторы зашиты в тесте и будут дублироваться в каждом тесте заново
- Дублирование кода создания веб-драйвера
- Assert не сопровожден сообщением об ошибке
- Если первый Assert «упадет», то второе условие вовсе не будет проверено
- При первом взгляде на тест не ясно, что в нем происходит, придется вчитываться и тратить время на понимание кода
Если подходить к автоматизации «в лоб», вместо того, чтобы избавиться от рутинного повторения одинаковых действий, мы получим дополнительную головную боль с поддержкой тестов, ложными срабатываниями и спагетти-кодом в придачу.
Слои приложения в автоматизированном тестировании
Ваши тесты – тоже код. Относитесь к ним также трепетно, как и коду в приложении. Тема слоев бизнес-приложений уже достаточно хорошо освещена. Какие слои можно выделить в тестах?
- Технический драйвер (WebDriver, Selenium RC, etc)
- Контекст тестирования (целевое окружение, пользователи, данные)
- Абстракция UI – страницы, виджеты, компоненты страниц (PageObject pattern)
- Тесты (тестовый фреймворк: NUnit, xUnit, MSTest)
- DSL
Проведем эволюционный рефакторинг и исправим наш тест.
Технический драйвер
В нашем случае, это Selenium.WebDriver. Сам по себе WebDriver – не инструмент автоматизации тестирования, а лишь средство управления браузером. Мы могли бы автоматизировать тестирование на уровне HTTP-запросов и сэкономить кучу времени. Для тестирования веб-сервисов нам вообще не потребуется веб-драйвер: прокси вполне достаточно.
Использование веб-драйвера хорошая идея потому что:
- Современные приложения – больше, чем просто запрос-ответ. Сессии, куки, java-script, веб-сокеты. Все это может быть чертовски сложно повторить программным образом
- Такое тестирование максимально приближено к поведению пользователя
- Сложность написания кода гораздо ниже
К слою технического драйвера относятся:
- Все настройки веб-драйвера
- Логика создания и уничтожения веб-драйвера
- Контроль ошибок
Для начала вынесем настройки в конфиг. У нас это выглядит так:
<driverConfiguration targetDriver="Firefox" width="1366" height="768" isRemote="false" screenshotDir="C:\Screenshots" takeScreenshots="true" remoteUrl="…"/>
Создадим отдельный класс, который возьмет на себя логику чтения конфига, создания и уничтожения веб-драйвера.
[Test] public void WebDriverContextGoogle_SearchGangnamStyle_PsyYouTubeChanelIsOnTop() { var wdc = WebDriverContext.GetInstance(); try { var wd = wdc.WebDriver; wd.Url = "http://google.com"; wd.Navigate(); wd.FindElement(By.Id("gbqfq")).SendKeys("gangnam style"); wd.FindElement(By.Id("gbqfb")).Click(); var firstResult = new WebDriverWait(wd, TimeSpan.FromSeconds(10)).Until( w => w.FindElement(By.CssSelector("h3.r>a"))); var expected = new KeyValuePair<string, string>( "PSY - YouTube", "http://www.youtube.com/user/officialpsy"); var actual = new KeyValuePair<string, string>( firstResult.Text, firstResult.GetAttribute("href")); Assert.AreEqual(expected, actual); } finally { wdc.Dispose(); } }
Стало немного лучше. Теперь мы уверены, что всегда-будет использоваться только один веб-драйвер. Все настройки в конфиге, поэтому мы можем менять драйвер и другие настройки без перекомпиляции.
Контекст тестирования
Для black-box тестирования приложения нам потребуется некоторое количество входных данных:
- Целевое окружение – url, порты тестируемых приложений
- Пользователи, с разным ролевым набором
Эта информация не относится к логике тестирования, поэтому мы вынесем это в конфигурационную секцию. Все окружения опишем в конфиге.
<environmentsConfiguration targetEnvironment="Google"> <environments> <environment name="Google" app="GoogleWebSite"> <apps> <app name="GoogleWebSite" url="http://google.com/" /> </apps> <users> <user name="Default" login="user" password="user" /> </users> </environment> </environmentsConfiguration>
Вместо wd.Url = «google.com»; стало wd.Url = EnvironmentsConfiguration.CurrentEnvironmentBaseUrl;
- Мы не должны больше дублировать URL во всех тестах
- Чтобы протестировать другое окружение достаточно собрать проект с другой конфигурацией и добавить трансформацию
<environmentsConfiguration targetEnvironment="Google-Test" xdt:Transform="SetAttributes">
Page Objects
Паттерн Page Objects хорошо зарекомендовал себя в автоматизации тестирования.
Основная идея – инкапсулировать логику поведения страницы в классе страницы. Таким образом, тесты будут работать не с низкоуровневым кодом технического драйвера, а с высокоуровневой абстракцией.
Основные преимущества Page Objects:
- Разделение полномочий: вся «бизнес-логика» страницы должна помещаться в Page Objects, классы тестов лишь вызывают публичные методы и проверяют результат
- DRY – все локаторы помещаются в одном месте.
Есликогда UI изменится, то мы изменим локатор лишь в одном месте - Скрытие слоя технического драйвера. Ваши тесты будут работать с высокоуровневой абстракцией. В будущем, возможно, вы захотите сменить драйвер: например, использовать PhantomJS, или вообще для каких-то участков отказаться от использования WebDriver, для улучшения производительности. В этом случае, вам придется заменить только код Page Objects. Тесты останутся неизменными
- Page Objects позволяет записать локаторы в декларативном стиле
Чего не хватает в Page Objects
Канонический паттерн предполагает создание одного класса на страницу вашего приложения. Это может быть неудобно в ряде случаев:
- Кастомизируемый и/или динамически-изменяемый лейаут
- Виджеты или иные элементы, присутствующие на многих страницах
Частично эти проблемы можно решить с помощью наследования, но агрегация видится предпочтительнее, как с технической точки зрения, так и c точки зрения понимания кода.
Поэтому лучше воспользоваться расширенной версией паттерна – Page Elements. Page Elements – позволяет дробить страницу на более мелкие составляющие – блоки, виджеты и т.д. После чего эти блоки можно переиспользовать в нескольких страницах.
Создадим страницу:
[FindsBy(How = How.Id, Using = "gbqfq")] public IWebElement SearchTextBox { get; set; } [FindsBy(How = How.Id, Using = "gbqfb")] public IWebElement SubmitButton { get; set; } public GoogleSearchResults ResultsBlock { get; set; } public void EnterSearchQuery(string query) { SearchTextBox.SendKeys(query); } public void Search() { SubmitButton.Click(); }
И «виджет» с результатами
public class GoogleSearchResults : PageElement { [FindsBy(How = How.CssSelector, Using = "h3.r>a")] public IWebElement FirstLink { get; set; } public KeyValuePair<string, string> FirstResult { get { var firstLink = PageHelper.WaitFor<GoogleSearchResults>(w => w.FirstLink); return new KeyValuePair<string, string>(firstLink.Text, firstLink.GetAttribute("href")); } } }
В NuGet есть пакет WebDriver.Support с прекрасным методом PageFactory.InitElements.
Метод хорош, но имеет побочные эффекты. PageFactory из пакета WebDriver.Support возвращает прокси и не дожидается загрузки элемента. При этом, если все методы синхронизации работают с классом By, который пока не умеет работать с атрибутом FindsBy.
Эта проблема решается созданием базового класса Page.
/// <summary> /// Get Page element instance by type /// </summary> /// <typeparam name="T">Page element type</typeparam> /// <param name="waitUntilLoaded">Wait for element to be loaded or not. Default value is true</param> /// <param name="timeout">Timeout in seconds. Default value=PageHelper.Timeout</param> /// <returns>Page element instance</returns> public T GetElement<T>(bool waitUntilLoaded = true, int timeout = PageHelper.Timeout) where T : PageElement /// <summary> /// Wait for all IWebElement properies of page instance to be loaded. /// </summary> /// <param name="withElements">Wait all page elements to be loaded or just load page IWebElement properties</param> /// <returns>this</returns> public Page WaitUntilLoaded(bool withElements = true)
Для того, чтобы реализовать метод WaitUntilLoaded достаточно собрать все публичные свойства с атрибутами FindBy и воспользоваться классом WebDriverWait. Я опущу техническую реализацию этих методов. Важно, что на выходе мы получим простой и изящный код:
var positionsWidget = Page.GetElement<GoogleSearchResults>();
Остался последний неудобный случай. Существуют некоторые виджеты, которые скрывают/показывают часть элементов в зависимости от состояния. Разбивать такой виджет на несколько с одним свойством каждый – нецелесообразно.
Решение тоже нашлось.
public static IWebElement WaitFor<TPage>( Expression<Func<TPage, IWebElement>> expression, int timeout = Timeout) var firstLink = PageHelper.WaitFor<GoogleSearchResults>(w => w.FirstLink);
Не буду утомлять технической реализаций этих методов. Давайте посмотрим, как будет выглядеть код после рефакторинга.
[Test] public void Google_SearchGangnamStyle_PsyYouTubeChanelIsOnTop() { try { var page = WebDriverContext.CreatePage<GooglePage>(EnvironmentsConfiguration.CurrentEnvironmentBaseUrl); page.EnterSearchQuery("gangnam style"); page.Search(); var expected = new KeyValuePair<string, string>( "PSY - YouTube", "http://www.youtube.com/user/officialpsy"); var actual = page.GetElement<GoogleSearchResults>().FirstResult; Assert.AreEqual(expected, actual); } finally { WebDriverContext.GetInstance().Dispose(); } }
На этом этапе стало гораздо лучше:
- Класс тестирования снял с себя управление драйвером и делегировал эти обязанности в класс страницы
- Мы избавились от дублирования локаторов
- Читаемость тестов улучшилась
Тесты
После того, как мы вынесли локаторы и логику в Page Objects, код тестов стал лаконичнее и чище. Однако несколько вещей до сих пор не очень хороши:
- Логика создания веб-драйвера дублируется из теста в тест
- Логика создания страницы в каждом методе тоже избыточна
- Магические строчки, «gangnam style», «PSY — YouTube», ”http://www.youtube.com/user/officialpsy” мозолят глаза
- Сам сценарий теста достаточно хрупок: результаты индексации могут измениться и нам придется менять код
Создадим базовый класс тестов
public class WebDriverTestsBase<T> : TestsBase where T:Page, new() { /// <summary> /// Page object instance /// </summary> protected T Page { get; set; } /// <summary> /// Relative Url to target Page Object /// </summary> protected abstract string Url { get; } [SetUp] public virtual void SetUp() { WebDriverContext = WebDriverContext.GetInstance(); Page = Framework.Page.Create<T>( WebDriverContext.WebDriver, EnvironmentsConfiguration.CurrentEnvironmentBaseUrl, Url, PageElements); } [TearDown] public virtual void TearDown() { if (WebDriverContext.HasInstance) { var instance = WebDriverContext.GetInstance(); instance.Dispose(); } } }
Перепишем тест еще раз
public class GoogleExampleTest : WebDriverTestsBase<GooglePage> { [Test] public void Google_SearchGangnamStyle_PsyYouTubeChanelIsOnTop() { Page.EnterSearchQuery("gangnam style"); Page.Search(); var expected = new KeyValuePair<string, string>( "PSY - YouTube", "http://www.youtube.com/user/officialpsy"); var actual = Page.GetElement<GoogleSearchResults>().FirstResult; Assert.AreEqual(expected, actual); } }
Уже почти идеально. Вынесем магические строки в атрибут TestCase и добавим комментарий к Assert’у
[TestCase("gangnam style", "PSY - YouTube", "http://www.youtube.com/user/officialpsy")] public void Google_SearchGoogle_FirstResult(string query, string firstTitle, string firstLink) { Page.EnterSearchQuery(query); Page.Search(); var expected = new KeyValuePair<string, string>(firstTitle, firstLink); var actual = Page.ResultsBlock.FirstResult; Assert.AreEqual(expected, actual, string.Format( "{1} ({2}) is not top result for query \"{0}\"", firstTitle, firstLink, query)); }
- Код теста стал понятным
- Повторяющиеся операции перенесены в базовый класс
- Мы предоставили достаточно информации, в случае падения теста все будет понятно из логов тест-ранера
- Можно добавить сколько угодно входных и выходных параметров без изменения кода теста с помощью атрибута TestCase
DSL
В этом коде осталась две проблемы:
- Код стал понятным и чистым, но чтобы его поддерживать в таком состоянии квалификация специалистов, поддерживающих тесты должна быть соответствующей
- У отдела QA, скорее всего есть свой тест-план, а наши авто-тесты пока с ним никак не коррелируют
- Часто одни и те же шаги повторяются сразу в нескольких сценариях. Избежать дублирования кода можно с помощью наследования и агрегации, но это уже кажется сложной задачей, особенно учитывая то, что порядок шагов может быть разным
- Google_SearchGangnamStyle_PsyYouTubeChanelIsOnTop(): CamelCase трудночитаем
С помощью плагина SpecFlow, мы можем решить эти проблемы. SpecFlow позволяет записать тестовые сценарии в Given When Then стиле, а затем, автоматизировать их.
Feature: Google Search As a user I want to search in google So that I can find relevent information Scenario Outline: Search Given I have opened Google main page And I have entered <searchQuery> When I press search button Then the result is <title>, <url> Examples: |searchQuery |title |url |gangnam style |PSY - YouTube |http://www.youtube.com/user/officialpsy [Binding] public class GoogleSearchSteps : WebDriverTestsBase<GooglePage> { [Given("I have opened Google main page")] public void OpenGooglePage() { // Page is already created on SetUp, so that's ok } [Given(@"I have entered (.*)")] public void EnterQuery(string searchQuery) { Page.EnterSearchQuery(searchQuery); } [When("I press search button")] public void PressSearchButton() { Page.Search(); } [Then("the result is (.*), (.*)")] public void CheckResults(string title, string href) { var actual = Page.GetElement<GoogleSearchResults>().FirstResult; var expected = new KeyValuePair<string, string>(title, href); Assert.AreEqual(expected, actual); } }
Таким образом:
- Каждый шаг можно реализовывать лишь однажды
- Атрибуты Given When Then поддерживают регулярные вырадения – можно создавать повторно использующиеся «функциональные» шаги
- QA-отдел может записывать сценарии в проектах авто-тестов
- Тестировщики могут писать DSL, а автоматизацию можно поручить программистам
- В любой момент времени отчет о пройденных тестах, а значит и количестве разработанного функционала, доступен на CI-сервере
Подробнее о SpecFlow и управлении требованиями с Given When Then можно прочитать в этой статье.
ссылка на оригинал статьи http://habrahabr.ru/post/178407/
Добавить комментарий