- PVSM.RU - https://www.pvsm.ru -
Автоматизация тестирования – место встречи двух дисциплин: разработки и тестирования. Наверное поэтому, я отношу эту практику к сложным, но интересным.
Путем проб и ошибок мы пришли к следующему технологическому стеку:
Для запуска тестов по расписанию мы используем TFS 2012 и TeamCity.
В статье я опишу, как мы к этому пришли, типовые ошибки и пути их решения.
Очевидно, что автоматизация тестирования имеет множество плюсов. Автоматическое решение:
Все, кто хоть раз занимался автоматизированным тестированиям, знают об оборотной стороне медали. Автоматические тесты могут быть:
Для примера, рассмотрим следующий код. По названию видно, что мы тестируем то, что по запросу «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();
}
}
В этом тесте очень много проблем:
Если подходить к автоматизации «в лоб», вместо того, чтобы избавиться от рутинного повторения одинаковых действий, мы получим дополнительную головную боль с поддержкой тестов, ложными срабатываниями и спагетти-кодом в придачу.
Ваши тесты – тоже код. Относитесь к ним также трепетно, как и коду в приложении. Тема слоев бизнес-приложений уже достаточно хорошо освещена. Какие слои можно выделить в тестах?
Проведем эволюционный рефакторинг и исправим наш тест.
В нашем случае, это Selenium.WebDriver. Сам по себе WebDriver – не инструмент автоматизации тестирования, а лишь средство управления браузером. Мы могли бы автоматизировать тестирование на уровне HTTP-запросов и сэкономить кучу времени. Для тестирования веб-сервисов нам вообще не потребуется веб-драйвер: прокси вполне достаточно.
Использование веб-драйвера хорошая идея потому что:
К слою технического драйвера относятся:
Для начала вынесем настройки в конфиг. У нас это выглядит так:
<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>
Вместо wd.Url = «google.com [1]»; стало wd.Url = EnvironmentsConfiguration.CurrentEnvironmentBaseUrl;
<environmentsConfiguration targetEnvironment="Google-Test" xdt:Transform="SetAttributes">
Паттерн Page Objects [2] хорошо зарекомендовал себя в автоматизации тестирования.
Основная идея – инкапсулировать логику поведения страницы в классе страницы. Таким образом, тесты будут работать не с низкоуровневым кодом технического драйвера, а с высокоуровневой абстракцией.
Основные преимущества 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, код тестов стал лаконичнее и чище. Однако несколько вещей до сих пор не очень хороши:
Создадим базовый класс тестов
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));
}
В этом коде осталась две проблемы:
С помощью плагина SpecFlow [3], мы можем решить эти проблемы. 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);
}
}
Таким образом:
Подробнее о SpecFlow и управлении требованиями с Given When Then можно прочитать в этой статье [4].
Автор: marshinov
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/33334
Ссылки в тексте:
[1] google.com: http://google.com
[2] Page Objects: https://code.google.com/p/selenium/wiki/PageObjects
[3] SpecFlow: http://www.specflow.org/specflownew/
[4] этой статье: http://habrahabr.ru/company/etnasoft/blog/166747/
[5] Источник: http://habrahabr.ru/post/178407/
Нажмите здесь для печати.