Qa

В чем состоит автоматизация тестирования

Используя различные подходы и примеры, пользуясь различными методами, приходим к такой последовательности действий: SpecFlow (опционально): DSL, затем NUnit: тестовый фреймворк, PageObject + PageElements: UI-абстракция и в заключении Selenium.WebDriver

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

Почему сложен этот путь

Все программисты осознают, что автоматизация тестирования состоит из многих положительных моментов: от экономии времени до исключения человеческого фактора, исключает монотонное регрессионное тестирование. Однако в этом опросе существуют скрытые подводные камни.

В тестировании встречаются негативные моменты — хрупкость и ломкость в связи с изменением UI; неправильное кодирование и внешнее влияние и многое другое.

Возьмем код, связанный с 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();
	}
}

Тест содержит множество негатива, от смешения слоев приложения до зашитых в тексте строк, а также дублирование кода локаторов и других проблем. Все это сулит любому программисту большие проблемы, и многое зависит от его терпения и усидчивости. На пути могут попасться ложные срабатывания и спагетти-код.

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

Тесты это тоже код, отнеситесь к ним с должным пиететом. Выделите слои технического драйвера, контекста тестирования, абстракцию UI, различные фреймворки и DSL. Следующий шаг связан с исправлением теста.

Техдрайвер

У нас техдрайвер называется Selenium.WebDriver, это средство управления браузером. Безусловно, если выполнить некоторые простые манипуляции в рамках процесса тестирования, то можно сэкономить большое количество времени, причем вполне достаточно Proxy. Однако применение web-driver будет хорошей идеей по ряду причин:

  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 тестирования необходимы различные данные, которые выносятся в конфигурационную секцию. Далее видим такую картину.

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

Теперь не нужно дублировать URL во всех тестах, достаточно собрать проект с другой конфигурацией.

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

Паттерн Page Objects

Паттерн хорошо показал себя в автоматизации. Он поможет тестам работать кодом техдрайвера с высокоуровневой абстракцией.

У Page Objects очень много преимуществ — разделение полномочий, размещение локаторов в одном месте, локаторы пишутся в декларативном стиле и многое другое.

Чего недостает в Page Objects

Данный паттерн создает один класс в странице, это никого не устраивает, возникают лейауты и другие проблемы.

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

[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 существует замечательный пакет с отличным методом, однако с обладанием побочных эффектов. Если реализовать все шаги, указанные ниже, то можно справится с проблемой

/// <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)

Показ технической реализации займет много места и времени, главное, в итоге мы приходим к простому и изящному коду:

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

Тут мы уже смогли добиться значительных улучшений, исчезли многие проблемы.

Тесты

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

Необходимо прийти к базовому классу тестирования

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

В итоге все стало более понятно, но нужно добиться идеала.

DSL

В коде все-таки остались проблемы, которые можно решить с использованием плагина SpecFlow. 

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. Нужно избегать проблемных локаторов

Неверно

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

 

2. Инкапсулируйте логику приложения в классах различных страниц
3. Необходимо выделять виджеты.
4. Следует группировать элементы в виджеты.
5. Нужно избегать магических строк в коде теста.
6. Следует выносить дублирующиеся операции в базовые классы тестов.
7. Требуется вынесение Page Objects в отдельную сборку.
8. Необходимо использовать 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. Применяйте высокоуровневые абстракции, к примеру:

Page.GetElement<GoogleSearchResults>();
var firstLink = PageHelper.WaitFor<GoogleSearchResults>(w => w.FirstLink);

12. Группировать шаги в DSL нужно семантически

Неверно

I have logged as a user with empty cart

Верно

I have logged in
And my cart is empty

13. В 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
14. Применяйте black-box тестирование.
15. Добейтесь того, чтобы результаты тестов были в общем доступе и их понимали все.