html текст
All interests
  • All interests
  • Design
  • Food
  • Gadgets
  • Humor
  • News
  • Photo
  • Travel
  • Video
Click to see the next recommended page
Like it
Don't like
Add to Favorites

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



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

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

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

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

Очевидно, что автоматизация тестирования имеет множество плюсов. Автоматическое решение:
  1. Экономит время
  2. Исключает человеческий фактор при тестировании
  3. Снимает бремя рутинного регрессионного тестирования

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

Для примера, рассмотрим следующий код. По названию видно, что мы тестируем то, что по запросу «gangnam 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 expected = new KeyValuePair<string, string>(title, href);
		var actual = Page.GetElement<GoogleSearchResults>().FirstResult;
		Assert.AreEqual(expected, actual);
	}
}

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

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

Гайдлайн автоматизатора


  1. Избегайте хрупких и сложных локаторов
    Не правильно:
    [FindsBy(How = How.XPath, Using = "((//div[@class='dragContainer']/div[@class='dragHeader']" +
           "/div[@class='dragContainerTitle'])[text()=\"Account Info\"])" +
           "/../div[@class='dragContainerSettings']")]
    public IWebElement SettingsButton { get; set; }
    

    Правильно:
    [FindsBy(How = How.Id, Using = "gbqfb")]        
    public IWebElement SubmitButton { get; set; }
    

    Лучше всего использовать id. От id может зависеть java-script и фронт-эндщики поменяют его с меньшей вероятностью. Если вы не можете использовать id (динамическая разметка), используйте data-aid (automation-id), или аналогичный атрибут
  2. Икапсулируйте логику вашего приложения в классах страниц (Page Objects), например LogonPage, RegistrationPage, HomePage, OrderPage и т.д.
  3. Выделяйте виджеты и повторяющиеся блоки в «виджеты» (Page Elements), например: Header, Footer, LogonLogoff
  4. Группируйте элементы в виджеты в зависимости от их отображения, например: ConfirmationPopup, EditPopup, AddPopup
  5. Избегайте магических строк в коде теста, выносите их в свойства страниц и виджетов или в хелперы, например OrderSuccessMessage, RegistrationSuccessMessage, InvalidPasswordMessage. Это позволит избежать лишнего кода ожидания загрузки/появления элементов
  6. Выносите повторяющиеся операции в базовые классы тестов, используйте SetUp, TearDown: создание и уничтожение драйвера, создание скриншотов с ошибками, аутентификация, улучшайте читаемость теста
  7. Вынесите Page Objects в отдельную сборку, это поможет избежать дублирования виджетов/страниц. Сборок с тестами может быть много
  8. Используйте Assert только в коде тестов, Assert’ы не должны содержаться в страницах и виджетах
  9. Используйте Assert’ы, лучше всего описывающие, что именно вы проверяете. Это улучшит читаемость теста
    Не правильно:
    var actual = Page.Text == “Success”
    Assert.IsTrue(actual);
    

    Правильно:
    Assert.AreEqual(MessageHelper.Success, Page.Text)
    

  10. Используйте сообщения об ошибках в Assert’ах
    Assert.AreEqual(MessageHelper.Success, Page.Text, “Registration process is not successfull”);
    

  11. Избегайте использования Thread.Sleep в качестве таймаута для загрузки элементов. Используйте высокоуровневые абстракции, гарантирующие загрузку необходимых DOM-элементов, например:
    Page.GetElement<GoogleSearchResults>();
    var firstLink = PageHelper.WaitFor<GoogleSearchResults>(w => w.FirstLink);
    

  12. Если вы не используете DSL пишите название тестов в формате [Страница_]Сценарий_Ожидаемое Поведение. Это поможет людям понять, какое именно поведение тестируется. Это может быть особенно важно при смене требований
  13. В DSL группируйте шаги семантически и так, чтобы максимизировать повторное использование шагов
    Не правильно:
    I have logged as a user with empty cart
    

    Правильно:
    I have logged in
    And my cart is empty
    

  14. В DSL сравнивайте кортежи одним шагом, это позволит писать более лаконичный код и получать больше информации при падении теста
    Не правильно:
    When I open Profile page
    I can see first name is “Patrick”
    And I can see last name is “Jane”
    And I can see phone is “+123 45-67-89”
    

    Правильно
    When I open Profile page
    I can see profile info: Patrick Jane +123 45-67-89
    

  15. Используйте black-box тестирование, вынесите инициализацию тестовых данных из кода тестов. Для этого хорошо подойдет, например SSDT-проект
  16. Используйте CI для регулярного запуска тестов, сделайте так, чтобы результаты тестов были общедоступны и понятны


Очень хороший доклад от eBay об автоматизации тестирования можно посмотреть здесь: www.youtube.com/watch?v=tJ0O8p5PajQ
Читать дальше
Twitter
Одноклассники
Мой Мир

материал с habrahabr.ru

1

      Add

      You can create thematic collections and keep, for instance, all recipes in one place so you will never lose them.

      No images found
      Previous Next 0 / 0
      500
      • Advertisement
      • Animals
      • Architecture
      • Art
      • Auto
      • Aviation
      • Books
      • Cartoons
      • Celebrities
      • Children
      • Culture
      • Design
      • Economics
      • Education
      • Entertainment
      • Fashion
      • Fitness
      • Food
      • Gadgets
      • Games
      • Health
      • History
      • Hobby
      • Humor
      • Interior
      • Moto
      • Movies
      • Music
      • Nature
      • News
      • Photo
      • Pictures
      • Politics
      • Psychology
      • Science
      • Society
      • Sport
      • Technology
      • Travel
      • Video
      • Weapons
      • Web
      • Work
        Submit
        Valid formats are JPG, PNG, GIF.
        Not more than 5 Мb, please.
        30
        surfingbird.ru/site/
        RSS format guidelines
        500
        • Advertisement
        • Animals
        • Architecture
        • Art
        • Auto
        • Aviation
        • Books
        • Cartoons
        • Celebrities
        • Children
        • Culture
        • Design
        • Economics
        • Education
        • Entertainment
        • Fashion
        • Fitness
        • Food
        • Gadgets
        • Games
        • Health
        • History
        • Hobby
        • Humor
        • Interior
        • Moto
        • Movies
        • Music
        • Nature
        • News
        • Photo
        • Pictures
        • Politics
        • Psychology
        • Science
        • Society
        • Sport
        • Technology
        • Travel
        • Video
        • Weapons
        • Web
        • Work

          Submit

          Thank you! Wait for moderation.

          Тебе это не нравится?

          You can block the domain, tag, user or channel, and we'll stop recommend it to you. You can always unblock them in your settings.

          • habrahabr.ru
          • домен habrahabr.ru

          Get a link

          Спасибо, твоя жалоба принята.

          Log on to Surfingbird

          Recover
          Sign up

          or

          Welcome to Surfingbird.com!

          You'll find thousands of interesting pages, photos, and videos inside.
          Join!

          • Personal
            recommendations

          • Stash
            interesting and useful stuff

          • Anywhere,
            anytime

          Do we already know you? Login or restore the password.

          Close

          Add to collection

             

            Facebook

            Ваш профиль на рассмотрении, обновите страницу через несколько секунд

            Facebook

            К сожалению, вы не попадаете под условия акции