Автоматизация тестирования Web-приложений

от автора

Автоматизация тестирования – место встречи двух дисциплин: разработки и тестирования. Наверное поэтому, я отношу эту практику к сложным, но интересным.

Путем проб и ошибок мы пришли к следующему технологическому стеку:

  1. SpecFlow (опционально): DSL
  2. NUnit: тестовый фреймворк
  3. PageObject + PageElements: UI-абстракиця
  4. Контекст тестирования (информация о целевом окружении, пользователях системы)
  5. Selenium.WebDriver

Для запуска тестов по расписанию мы используем TFS 2012 и TeamCity.
В статье я опишу, как мы к этому пришли, типовые ошибки и пути их решения.

Зачем так сложно?

Очевидно, что автоматизация тестирования имеет множество плюсов. Автоматическое решение:

  1. Экономит время
  2. Исключает человеческий фактор при тестировании
  3. Снимает бремя рутинного регрессионного тестирования

Все, кто хоть раз занимался автоматизированным тестированиям, знают об оборотной стороне медали. Автоматические тесты могут быть:

  1. Хрупкими и «ломаться» из-за изменения UI
  2. Непонятными, содержать код «с душком»
  3. Недостоверными: тестировать неверное поведение или зависеть от особенностей окружения

Для примера, рассмотрим следующий код. По названию видно, что мы тестируем то, что по запросу «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();             }         } 

В этом тесте очень много проблем:

  1. Перемешаны слои приложения (драйвер, локаторы, результаты)
  2. Строки зашиты в тесте
  3. Для того, чтобы изменить веб-драйвер, например на IE придется менять все тесты
  4. Локаторы зашиты в тесте и будут дублироваться в каждом тесте заново
  5. Дублирование кода создания веб-драйвера
  6. Assert не сопровожден сообщением об ошибке
  7. Если первый Assert «упадет», то второе условие вовсе не будет проверено
  8. При первом взгляде на тест не ясно, что в нем происходит, придется вчитываться и тратить время на понимание кода

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

Слои приложения в автоматизированном тестировании

Ваши тесты – тоже код. Относитесь к ним также трепетно, как и коду в приложении. Тема слоев бизнес-приложений уже достаточно хорошо освещена. Какие слои можно выделить в тестах?

  1. Технический драйвер (WebDriver, Selenium RC, etc)
  2. Контекст тестирования (целевое окружение, пользователи, данные)
  3. Абстракция UI – страницы, виджеты, компоненты страниц (PageObject pattern)
  4. Тесты (тестовый фреймворк: NUnit, xUnit, MSTest)
  5. DSL

Проведем эволюционный рефакторинг и исправим наш тест.

Технический драйвер

В нашем случае, это Selenium.WebDriver. Сам по себе WebDriver – не инструмент автоматизации тестирования, а лишь средство управления браузером. Мы могли бы автоматизировать тестирование на уровне HTTP-запросов и сэкономить кучу времени. Для тестирования веб-сервисов нам вообще не потребуется веб-драйвер: прокси вполне достаточно.
Использование веб-драйвера хорошая идея потому что:

  1. Современные приложения – больше, чем просто запрос-ответ. Сессии, куки, java-script, веб-сокеты. Все это может быть чертовски сложно повторить программным образом
  2. Такое тестирование максимально приближено к поведению пользователя
  3. Сложность написания кода гораздо ниже

К слою технического драйвера относятся:

  1. Все настройки веб-драйвера
  2. Логика создания и уничтожения веб-драйвера
  3. Контроль ошибок

Для начала вынесем настройки в конфиг. У нас это выглядит так:

<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 тестирования приложения нам потребуется некоторое количество входных данных:

  1. Целевое окружение – url, порты тестируемых приложений
  2. Пользователи, с разным ролевым набором

Эта информация не относится к логике тестирования, поэтому мы вынесем это в конфигурационную секцию. Все окружения опишем в конфиге.

	<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;

  1. Мы не должны больше дублировать URL во всех тестах
  2. Чтобы протестировать другое окружение достаточно собрать проект с другой конфигурацией и добавить трансформацию

<environmentsConfiguration targetEnvironment="Google-Test" xdt:Transform="SetAttributes"> 

Page Objects

Паттерн Page Objects хорошо зарекомендовал себя в автоматизации тестирования.
Основная идея – инкапсулировать логику поведения страницы в классе страницы. Таким образом, тесты будут работать не с низкоуровневым кодом технического драйвера, а с высокоуровневой абстракцией.

Основные преимущества Page Objects:

  1. Разделение полномочий: вся «бизнес-логика» страницы должна помещаться в Page Objects, классы тестов лишь вызывают публичные методы и проверяют результат
  2. DRY – все локаторы помещаются в одном месте. Если когда UI изменится, то мы изменим локатор лишь в одном месте
  3. Скрытие слоя технического драйвера. Ваши тесты будут работать с высокоуровневой абстракцией. В будущем, возможно, вы захотите сменить драйвер: например, использовать PhantomJS, или вообще для каких-то участков отказаться от использования WebDriver, для улучшения производительности. В этом случае, вам придется заменить только код Page Objects. Тесты останутся неизменными
  4. Page Objects позволяет записать локаторы в декларативном стиле

Чего не хватает в Page Objects

Канонический паттерн предполагает создание одного класса на страницу вашего приложения. Это может быть неудобно в ряде случаев:

  1. Кастомизируемый и/или динамически-изменяемый лейаут
  2. Виджеты или иные элементы, присутствующие на многих страницах

Частично эти проблемы можно решить с помощью наследования, но агрегация видится предпочтительнее, как с технической точки зрения, так и 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();             }         }  

На этом этапе стало гораздо лучше:

  1. Класс тестирования снял с себя управление драйвером и делегировал эти обязанности в класс страницы
  2. Мы избавились от дублирования локаторов
  3. Читаемость тестов улучшилась

Тесты

После того, как мы вынесли локаторы и логику в Page Objects, код тестов стал лаконичнее и чище. Однако несколько вещей до сих пор не очень хороши:

  1. Логика создания веб-драйвера дублируется из теста в тест
  2. Логика создания страницы в каждом методе тоже избыточна
  3. Магические строчки, «gangnam style», «PSY — YouTube», ”http://www.youtube.com/user/officialpsy” мозолят глаза
  4. Сам сценарий теста достаточно хрупок: результаты индексации могут измениться и нам придется менять код

Создадим базовый класс тестов

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));         } 

  1. Код теста стал понятным
  2. Повторяющиеся операции перенесены в базовый класс
  3. Мы предоставили достаточно информации, в случае падения теста все будет понятно из логов тест-ранера
  4. Можно добавить сколько угодно входных и выходных параметров без изменения кода теста с помощью атрибута TestCase

DSL

В этом коде осталась две проблемы:

  1. Код стал понятным и чистым, но чтобы его поддерживать в таком состоянии квалификация специалистов, поддерживающих тесты должна быть соответствующей
  2. У отдела QA, скорее всего есть свой тест-план, а наши авто-тесты пока с ним никак не коррелируют
  3. Часто одни и те же шаги повторяются сразу в нескольких сценариях. Избежать дублирования кода можно с помощью наследования и агрегации, но это уже кажется сложной задачей, особенно учитывая то, что порядок шагов может быть разным
  4. 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);         }     } 

Таким образом:

  1. Каждый шаг можно реализовывать лишь однажды
  2. Атрибуты Given When Then поддерживают регулярные вырадения – можно создавать повторно использующиеся «функциональные» шаги
  3. QA-отдел может записывать сценарии в проектах авто-тестов
  4. Тестировщики могут писать DSL, а автоматизацию можно поручить программистам
  5. В любой момент времени отчет о пройденных тестах, а значит и количестве разработанного функционала, доступен на CI-сервере

Подробнее о SpecFlow и управлении требованиями с Given When Then можно прочитать в этой статье.

ссылка на оригинал статьи http://habrahabr.ru/post/178407/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *