Руководство по приготовлению бутербродов из Selenium. Часть 2 – Проектируем фреймворк

3. Проектируем фреймворк

Во время написания тестов на Selenium IDE мы постоянно делали “заметки на полях” и думали, как сделать процедуру написания тестов максимально легкой, простой и удобной, а сопровождение тестов не создавало столько хлопот. В дальнейшем, все эти качества в последствии приобрел ButerbroD. Почему было принято решение разрабатывать собственный фреймворк на Selenium RC:

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

Помимо этого, сам фреймворк должен быть удобно расширяемым и еще одно важное требование – в тестах должно быть минимум программирования. Подразумевалось, что авто-тесты будут писать “ручные” тестировщики, не все из них обладают глубокими техническими навыками. В общем, работа над фреймворком закипела.

 

Ингредиенты. В качестве среды разработки мы выбрали Microsoft Visual Studio, язык программирования C#. Выбор был обусловлен технологиями, используемыми при разработке продуктов в компании, чтобы в случае чего мы могли обращаться за помощью за высококвалифицированной помощью :-) . Глупо начинать писать фреймворк на Python, если в вашей компании программируют, к примеру, только на PHP. В качестве библиотеки для тестирования используется MS Test, хотя начинали работать с NUnit. О достоинствах вышеперечисленных фреймворков можно долго рассуждать, я считаю, что их возможности примерно равны – у каждого свои плюсы и минусы. Мы остановились на MS Test, так как он полностью интегрирован в Visual Studio и обладает мощными дополнениями. К слову, переход с MSTest на NUnit, и наоборот, не требует особых усилий. На своих новых проектах я использую NUnit. Окончательно выбрав инструментарий и еще раз обсудив все “хотелки”, мы принялись за архитектуру фреймворка. Общая структура практически любого фреймворка автоматизации выглядит примерно так:

Общая структура. Из чего же состоит ButerbroD? Общая структура ButerbroD-а:

Драйвер веб-интерфейса (Selenium RC) выносится в отдельный слой, для того, чтобы в случае необходимости его можно было без проблем сменить. Например, WebDriver на WatiN. Как-никак существуют и другие веб-драйверы, не только Selenium. Поэтому изолируйте в отдельный слой все упоминание о Selenium, чтобы в случае чего от него можно было безболезненно от него отказаться. Возможно, завтра выйдет в свет инструмент автоматизации «Циркониум» или “Алюминиум”, который окажется мощнее существующих решений. Нужно быть готовым к смене драйвера. Хотя, не нужно быть совсем уже параноиком :-) и разрабатывать “космическое” решение.

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

Сам ButerbroD содержит следующие слои: Selenium Document, Document, Pages, Elements, Actions (Atomic, Composite), Test Cases, UITests. Все слои фреймворка далее будут подробно описаны. ButerbroD в разрезе:

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

Selenium Document. Создаём слой, в котором имплементируются команды Selenium-а. Данный уровень является адаптером над Selenium, именно через данный уровень происходит общение фреймворка с Selenium. По своей сути, Selenium Document, представляет собой набор методов-оберток над командами Selenium. Это значит, что команды Selenium-а фактически задействованы только на этом уровне. Не нужно оборачивать абсолютно все команды Selenium-а, достаточно создать только необходимые методы, остальные будут добавляться по мере необходимости. Пример метода Click, в данном случае к команде Selenium добавлена трассировка и все это обернуто методом ButerbroD-а:

        public override void Click (BaseComponent component)
        {
            Trace.WriteLine("Clicking on: '" + component+"'.", "Document");
            StateS.GetTestState().Selenium.Click(component.Constraint.ToLocator());
        }

На данном уровне мы объединяем (группируем) часто использующиеся команды для более удобного их использования, а также создаем собственные вспомогательные методы для работы с Selenium. Пример ClickAndWaitForPageToLoad:

 

        public override void ClickAndWaitForPageToLoad (BaseComponent component)
        {
            Trace.WriteLine("Clicking on: '" + component+"'.", "Document");
            StateS.GetTestState().Selenium.Click(component.Constraint.ToLocator());
            Trace.WriteLine("Waiting for page to load.", "Document");
            StateS.GetTestState().Selenium.WaitForPageToLoad(Config.MomentStr);
        }

Имена команд, страниц, действий и тестов должны содержать максимум информации. Фактически, мы начинаем формировать свой доменный язык. Данный подход называется DSL (Domain-specific language). То есть, фактически мы создаем свой язык, который будет помогать нам эффективно решать наши задачи в рамках нашего фреймворка. Суть DSL паттерна заключается в том, чтобы названия методов, классов, переменных и т.д. содержали максимум доменной информации и глядя на них любой человек знакомый с данной доменной областью мог разобраться, что происходит в тесте. Создавайте побольше собственных вспомогательных методов, “оберните” команды Selenium-а в подходящий для вас вид. Данный подход соблюдается на всех уровнях ButerbroD-а. DSL позволяет писать тесты, практически не имея особых знаний в программировании. Чем меньше программирования в тестах, тем они проще и больше людей могут принимать участие в их разработке. Помимо этого мы добиваемся возможности повторного использования кода. Избегайте дублирования кода – выносите все в отдельные методы, а также объединяете схожие по задумке и функциональности методы. Не размножайте методы которые предназначены для одного и того же.

Базовым классом Selenium Document является абстрактный класс Document, он не имеет привязки к Selenium. По своей сути он является прокси/адаптером к Selenium Document. Он содержит абстрактные методы класса Selenium Document. В случае отказа от Selenium мы можем использовать его как шаблон для набивки команд любого драйвера веб-интерфейса.

Точка входа. Инициализация Selenium локализована от самих тестов. Класс, который отвечает за инициализацию Selenium RC называется StateS. Все тесты выполняются на отдельно выделенном сервере. Перед запуском принудительно прибиваем все браузерные процессы, чтобы не было сбоев во время прохождения тестов. Вот так может выглядеть метод запуска Selenium RC:

 

   public StateS(string host, int port, string browser, string url)
        {
            KillBrowser();   //// Kills a browser process.
            Processor = new HttpCommandProcessor(host, port, browser, url);
            Selenium = new DefaultSelenium(Processor);
            Selenium.Start();
            Selenium.WindowMaximize();
            Selenium.WindowFocus();
        }

Конфигурация. Все конфигурационные настройки вынесены в отдельный xml файл, который гордо называется app.config. Пример простого конфигурационного файла:

<?xml version="1.0" encoding="utf-8"?>
       <configuration>
       <appSettings>
       <add key="host" value="localhost"/>
       <add key="port" value="4444"/>
       <add key="browser" value="*chrome"/>
       <add key="url" value="https://bugscatcher.net/"/>
       <add key="moment" value="600000"/>
       <add key="sec" value="10"/>
       <add key="BrowserProcessName" value="Firefox"/>
       <add key="EnableValidationChecks" value="True"/>
       </appSettings>
       </configuration>

В данном файле, в первую очередь содержатся настройки Selenium RC, а также некоторые другие. Отдельно отмечу опцию EnableValidationChecks, при отрицательном значении которой все необязательные проверки (они отмечены в тестах) не будут выполняться. Тем самым мы экономим время для прогона тестов в некоторых ситуациях. Очень помогает в случае, когда нужно быстро получить результат выполнения тестов. С помощью данной опции на нашем проекте проверяются отличные от английской локализации. То есть текстовые элементы не проверяются, проверяется только сам результат выполнения действий. Также у нас есть еще один файл конфигурации, в котором хранятся статические тестовые данные – аватары, файлы-вложения и так далее.

Страницы. Из чего состоит веб-приложение? Любое веб-приложение состоит из набора страниц, которые как кирпичики складываются в единое целое. У каждой страницы есть набор атрибутов – адрес, заголовок, текст, элементы. Данные атрибуты есть у любой страницы, но все они разные. Пример страницы:

Очень часто можно встретить ситуацию в тесте, когда отсутствует привязка к реальным страницам приложения. Это приводит к тому, что тесты слишком сложны для понимания – с ходу нельзя разобраться, что происходит в тесте, не заглядывая в тестируемое приложение. В такие тесты сложно вносить изменения. Еще одна характерная проблема – организация работы с локаторами (идентификаторы элементов). Часто можно столкнуться и с такой ситуацией, когда локаторы не несут никакой информации. Представим, что у нас сломался тест – не находится элемент страницы и его нужно быстро починить. Открываем тесты, находим строку, в которой содержится ошибка, смотрим, что у нас в этой строке выполняется некоторая команда над элементом, название которого нам абсолютно ничего не говорит. Тут становится интересно, а что же происходит в тесте. Особенно плохи дела, если локатор представляет собой сто символьный xpath локатор (и это еще цветочки!). Приходится копать совсем глубоко. Гораздо удобнее, когда все локаторы вынесены в отдельный файл и имеют человеческие названия. Но есть решение еще лучше.

Использование паттерна Page Object позволяет избежать ранее описанных проблем и сделает работу по сопровождению тестов легкой и быстрой. Данный паттерн способен разделить логику тестов от специфичной для конкретной страницы информации, создать абстракцию для страницы/формы тестируемого вами приложения. Как результат, мы должны получить класс описывающий страницу – ее свойства и элементы, специфические для данной страницы проверки. Этот дает нам возможность разделить высокоуровневую тестовую (шаги теста) логику от низкоуровневой логики (поиск элементов, базовые проверки страниц и т.д.).

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

  • Open() – открытие страницы;
  • Validate() – базовые проверки страницы (элементы, текст);
  • ValidateNoWait() – см. Validate(), только в данном случае не будет происходить ожидание загрузки страницы;
  • ValidateTitle(string title) – проверить заголовок страницы;
  • ValidateControls() – проверка только элементов страницы;
  • ValidateText() – проверка только текстовых элементов страницы;
  • CheckGeneralErrors() – базовые проверки на наличие ошибок характерных для всего приложения.

Далее все страницы мы можем проектировать по одному образу и подобию. Пример страницы входа в администраторскую часть WordPress:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }public class LoginPage : Page { public override Uri RelativeUrl { get { return new Uri("wp-login.php", UriKind.Relative); } } public override void ValidateTitle(string title) { base.ValidateTitle(title); Assert.AreEqual("Bugs Catcher › Log In", title); }
    public TextField UserNameTextField { get { return this.Document.FindById&lt;TextField&gt;("user_login"); } }
    public TextField PasswordTextField { get { return this.Document.FindById&lt;TextField&gt;("user_pass"); } }
    public Button LogInButton { get { return this.Document.FindById&lt;Button&gt;("wp-submit"); } }
    public ToggleButton RememberMeCheckBox { get { return this.Document.FindById&lt;ToggleButton&gt;("rememberme"); } }
    public Button LostYourPasswordButton { get { return this.Document.FindByLink&lt;Button&gt;("Lost your password?"); } }
    public Button BackToBugsCatcherButton { get { return this.Document.FindByLink&lt;Button&gt;("← Back to Bugs Catcher"); } }

    public override void ValidateControls()
    {
        if (TestExecutionContext.EnableValidationChecks)
        {
            Trace.WriteLine("Assert controls on 'Log In' page:", "Page");
            Document.AssertPresent(this.UserNameTextField);
            Document.AssertPresent(this.PasswordTextField);
            Document.AssertPresent(this.LogInButton);
            Document.AssertPresent(this.RememberMeCheckBox);
            Document.AssertPresent(this.LostYourPasswordButton);
            Document.AssertPresent(this.BackToBugsCatcherButton);
        }
    }

    public override void ValidateText()
    {
        if (TestExecutionContext.EnableValidationChecks)
        {
            Trace.WriteLine("Assert text elements on 'Log In' page:", "Page");
            Document.AssertTextPresent("Username");
            Document.AssertTextPresent("Password");
            Document.AssertTextPresent("Remember Me");
            Document.AssertTextPresent("Lost your password?");
            Document.AssertTextPresent("← Back to Bugs Catcher");
        }
    }

    public void AssertIsLoginCorrectly ()
    {
        Trace.WriteLine("Check is 'Login' correctly:", "Page");
        Document.AssertTextIsNotPresent("ERROR: Invalid username.");
        Document.AssertTextIsNotPresent("ERROR: The password you entered for the username");
    }
}

}{/syntaxhighlighter}

В данном примере для LoginPage, мы указали адрес и заголовок страницы.
Далее следует описание всех элементов страницы. Даем элементам страницы
понятные названия. Глядя на данные названия элементов у нас нет
необходимости отрывать приложение и смотреть, что за элемент мы
используем в тесте. Обратите внимание, что мы также указываем тип
элемента (кнопка, текстовое поле и т.д.), к этому я вернусь чуть
подробнее позже. Помимо этого указываем механизм поиска элемента. Обычно
я работаю с id, xpath, css и ссылками. Похожий механизм поиска
элементов реализовали в Selenium WebDriver:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }Driver.FindElement(By.Id(“LoginControl_LoginButton”));{/syntaxhighlighter}


Данная страница содержит базовые проверки ValidateControls и ValidateText, которые выполняются при ее открытии, их можно вызывать и напрямую из тестов в случае необходимости. Инструкция TestExecutionContext.EnableValidationChecks означает, что данная проверка не будет выполняться, если опция EnableValidationChecks
в файле конфигурации отключена. На уровне страницы должны содержаться
все проверки и вспомогательные действия, которые относятся к данной
странице.

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

Пример методов для работы с компонентами типа radio button и check box:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }public class ToggleButton : BaseComponent { /// <summary> /// Activate toggle-button (checkbox/radio) and wait for Ajax requests. /// </summary>
    public void Activate()
    {
        Trace.WriteLine("Activate toggle-button: '" + this + "':", "Element");
        Document.WaitForElementPresent(this);
        if (Document.IsChecked(this))
        {
            Trace.WriteLine("Toggle-button '" + this + "' is checked. No action performed.", "Element");
        }
        else
        {
            Document.Check(this);
            Document.WaitForAjaxRequests();
            Trace.WriteLine("Toggle-button: '" + this + "' is activate.", "Element");
        }
    }

    /// &lt;summary&gt;
    /// Assert is toggle-button (checkbox/radio) is checked.
    /// &lt;/summary&gt;

    public void AssertIsChecked()
    {
        Trace.WriteLine("Assert is toggle-button '" + this + "' is checked:", "Element");
        Document.WaitForElementPresent(this);
        Assert.IsTrue(Document.IsChecked(this), "Error! Toggle-button '" + this + "' is not checked!");
    }

    ///  &lt;summary&gt;
    /// Assert is toggle-button (checkbox/radio) is not checked.
    /// &lt;/summary&gt;

    public void AssertIsNotChecked()
    {
        Trace.WriteLine("Assert is toggle-button '" + this + "' is not checked:", "Element");
        Document.WaitForElementPresent(this);
        Assert.IsFalse(Document.IsChecked(this), "Error! Toggle-button '" + this + "' is checked!");
    }

    /// &lt;summary&gt;
    /// Check a toggle-button (checkbox/radio) and wait for Ajax requests.
    /// &lt;/summary&gt;

    public void Check()
    {
        Trace.WriteLine("Check toggle-button '" + this + "':", "Element");
        Document.WaitForElementPresent(this);
        Document.Check(this);
        Document.WaitForAjaxRequests();
    }

    /// &lt;summary&gt;
    /// Deactivate a toggle-button (checkbox/radio) and wait for Ajax requests.
    /// &lt;/summary&gt;

    public void Deactivate()
    {
        Trace.WriteLine("Deactivate toggle-button '" + this + "':", "Element");
        Document.WaitForElementPresent(this);
        if (Document.IsChecked(this))
        {
            Trace.WriteLine("Toggle-button: '" + this + "' is deactivated.", "Element");
            Document.Click(this);
            Document.WaitForAjaxRequests();
        }
        else
        {
            Trace.WriteLine("Element: '" + this + "' is deactivated. No action performed.", "Element");
        }
    }

    /// &lt;summary&gt;
    /// Gets whether a toggle-button (checkbox/radio) is checked. No assert!
    /// &lt;/summary&gt;

    public bool IsChecked
    {
        get
        {
            Trace.WriteLine("Gets whether a toggle-button '" + this + "' is checked:", "Element");
            Document.WaitForElementPresent(this);
            return Document.IsChecked(this);
        }
    }
}{/syntaxhighlighter}<p style="text-indent: 20px; text-align: left;">Шаблон Page Object и отдельный слой элементов позволяют на уровне шагов 

использовать предельно простые и понятные команды. Как пример, метод
заполнения поля «User Name» значением переменной «Username» для текущей
страницы.

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }this.Page().UserNameTextField.TypeText = Username;{/syntaxhighlighter}


После формирования страниц и элементов можно начать создавать действия
(Actions). В ButerbroD действия бывают двух типов – атомарные (Atomic) и
составные (Composite).

Атомарные действия (атомики). Цель данного слоя сгруппировать и организовать действия над элементами страниц, обернуть их в автономные шаги. Шаг – это минимальное логическое действие. Его размер зависит от логики построения ваших тестов. Я рекомендую делать шаги маленькими, так как чем меньше шаг, тем более маневренные тесты мы можем создавать. Атомарные действия являются компонентами для составных действий. В ButerbroD-е существует два типа атомарных действий: IStep и IAssertableStep.

Интерфейс IStep означает, что данный шаг будет задействован в качестве основного кирпича составного действия. Пример метода открытия страницы:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }sealed class Open : IStep, IPageStep<LoginPage> { public void Invoke() { Trace.WriteLine("Opening 'Login' page:", "Atomic"); this.Page().Open(); } }{/syntaxhighlighter}

Метод Open() включает в себя базовые проверки открываемой страницы, поэтому дополнительные ассерты не нужны. Интерфейс IAssertableStep означает, что данный шаг еще должен содержать проверку DoAssert() для проверки выполнения действия. Пример выбора значения из выпадающего списка для поля «SetType».

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; } sealed class SetType : IAssertableStep, IPageStep<Pages.ActivityDetails> { public string Type { get; set; }
    public void Invoke()
    {
        Trace.WriteLine("Set activity 'Type':", "Atomic");
        this.Page().ActivityTypeList.SelectFromTelerikCombo = this.Type;
    }

    public void DoAssert()
    {
        Assert.AreEqual(this.Type, this.Page().ActivityTypeList.SelectFromTelerikCombo);
    }
}{/syntaxhighlighter}<p style="text-indent: 20px;" align="justify">Как должен выглядеть, обычный “ручной″ тест-кейс:</p>

Составные действия (композиты). Как и обычный тест-кейс, составные действия в нашем фреймворке состоят из несколько частей: подготовительные действия (start steps), основные шаги теста с проверками (details steps) и завершающие действия (final steps). Задача составных действий – сгруппировать атомарные действия для повторного использования, по сущностям. То есть мы создаем общий скелет для некоторой группы тестов для определенной сущности нашего приложения. Пример составного действия «Create activity»:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; } protected override IStep Build() { return this.TraceLine("Create a Activity:", "Composite")
            .Then&lt;Atomic.ActivityList.Open&gt;()
            .Then&lt;Atomic.ActivityList.CreateANewActivity&gt;()
            .Then&lt;Atomic.ActivityDetails.ShouldBeCurrent&gt;()
            .Then(this.DetailsSteps)
            .Then(this.FinalStep)
            .Then(this.DetailsSteps.Asserts());
    }{/syntaxhighlighter}<p style="text-indent: 20px;" align="justify">

Основой для составных действий являются подготовительные шаги для
некоторой группы тестов. Таким образом, нам не нужно дублировать код при
написании тестов для одной сущности. В данном случае подготовительными
шагами, которые характерны для определенной группы тестов являются
методы: Open, CreateANewActivity, ShouldBeCurrent.
После подготовительных действий следует сам тест, который мы будем
формировать на более высоком уровне тест-кейсов. Чем наиболее
качественно мы разработаем составные и атомарные действия, тем более
гибкими получится наши тест-кейсы. Ведь основные шаги теста можно
вызывать в разной последовательности, чередовать набор шагов и т.д. Это
все делает наши тесты максимально маневренными.

На данном уровне задача – сформировать необходимые нам основные действия, детализирующие тест. Данные шаги можно группировать в логические наборы из атомарных действий, например – заполнение обязательных полей форм, или метод для заполнения всех полей формы. Создадим метод для заполнений полей «Name» и «Prоject» на форме создания новой активности, назовем метод WithNameAndProject:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; } public Create WithNameAndProject(string activity_name, string project_name) { var newDetailsSteps = new List<IAssertableStep>(this.DetailsSteps);
        newDetailsSteps.AddOrReplace(new Atomic.ActivityDetails.SetActivityName { ActName = activity_name });
        newDetailsSteps.AddOrReplace(new Atomic.ActivityDetails.SetProjectName { ProjectName = project_name });

        return new Create
        {
            DetailsSteps = newDetailsSteps,
        };
    }{/syntaxhighlighter}<p style="text-indent: 20px;" align="justify">Вот еще один пример DetailsStep-а на уровне составных действий:</p><p style="text-indent: 20px;" align="justify">&nbsp;</p>{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }   public Edit ClickImDoneButton(string User)
    {
        var newDetailsSteps = new List&lt;IAssertableStep&gt;(this.DetailsSteps);

        newDetailsSteps.Add(new Atomic.ActivityDetails.ImDone { UserName = User });

        return new Edit
        {
            DetailsSteps = newDetailsSteps,
        };
    }{/syntaxhighlighter}<p style="text-indent: 20px;" align="justify">

Далее следует FinalStep, в котором по умолчанию происходит сохранение
формы. Но при желании можно финальное действие переопределить, например,
вызвать метод для закрытия формы. Такой подход расширяет возможности
составного действия и позволяет повторно использовать код. После
финального действия выполнится шаг DetailsSteps.Asserts(). Данная операция вызовет все методы DoAssert(),
указанные в атомарных действиях. То есть это возможность убедиться, что
после сохранения формы все данные будут сохранены. Отмечу, что если
проверка нужна сразу после выполнения действия, а не после какого-то
действия (в данном случае Save), то нужно указать эту проверку
непосредственно после действия в атомике. Обычно, при работе с
текстовыми полями формы я проверяю, что верное значение отображается
сразу после ввода значения. А также, после сохранения формы. ButerbroD
достаточно гибок, поэтому если вы не хотите лишних проверок, то вы их
просто можете не создавать. Используйте проверки только там, где они
нужны! После формирования составных действий остается только разработать
тест-кейсы.

Итак, составное действие состоит из следующих составляющих:

  • Start step – группа шагов, которые всегда выполняется перед некоторой группой тестов. Например: логин, открытие страницы, нажатие на кнопку. После данной последовательности будут выполняться необходимые для конкретного тест-кейса шаги;
  • Details step – это конкретизирующие шаги тест-кейса. Пример – заполнение полей, нажатие на кнопки. Для конкретного тест-кейса набор details steps могут быть разными, последовательность их вызова может быть совершенно любой;
  • Final step – группа шагов, которыми всегда заканчивается тест. Пример – нажатие на кнопку «Сохранить», нажатие на кнопку «Закрыть», выход из системы.

Общая схема организации действий в ButerbroD:

Тест кейсы. На данном уровне группируются вызовы составных действий, конкретизируются шаги теста и указываются тестовые данные. Данные изолированы от самой логики тестов и появляются только на этом уровне. Примеры тест-кейсов для создания активности:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; } public static CreateActivity CreateActivity_RequestOfTheLastProjectByDuration() { return new CreateActivity() {
            LoginCase = Login.AsAdministrator,
              CreateStep = new Actions.Composite.Activity.Create()
                .WithAttach(InputDataConf.AttachSmallFile, "attach for a project created by Admin")
                .WithNameAndProject("Request of The last project by duration", "The last project")
                .WithStartDateWithShift(0, +1, 0)
                .WithType("Request")
                .WithPriority("Urgent")
                .WithDuration ("55")
                .WithProgress ("80")
                .WithEstimatedHours("1.00")
                .WithMember("Administrator")
                .WithMember("Den")
                .WithMember("Project Manager")
        };
    }{/syntaxhighlighter}<p style="text-indent: 20px;" align="justify">А можно создать активность и следующим образом:</p><p style="text-indent: 20px;" align="justify">&nbsp;</p>{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }public static CreateActivity CreateActivity_ForProjectMessageboardTest()
    {
        return new CreateActivity()
        {
            CreateStep = new Actions.Composite.Activity.Create()
                .WithStartDateWithShift(0, 0, -2)
                .WithEndDateWithShift(0, +1, 0)
                .WithProject("ProjectForProjectMessageboardTest")
                .WithName("ActivityForProjectMessageboardTest")
        };
    }{/syntaxhighlighter}<p style="text-indent: 20px;" align="justify">

Как видим, авто-тесты максимально схожи с тестами, которые прогоняются
тестировщиками вручную. Можно легко менять последовательность команд, а
также создавать идентичные тесты, варьируя при этом входные данные.
Фактически, любой член команды может запросто разобраться в тестах и
создавать свои. Графическое изображение тест-кейса:

UITest. На уровне UITest происходит вызов тест-кейсов и формирование тестовых наборов. Вот так может выглядеть вызов простого теста:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; } [TestMethod] public void UserEdit() { TestCases.CreateUser.CreateUser_Polaris().Invoke(); TestCases.EditUser.EditUser_Polaris().Invoke(); }{/syntaxhighlighter}

Логирование. Каждый уровень фреймворка логируется, а затем форматируется в читабельный вид. Также логируется каждое действие на уровне атомарных действий. Так как в ButerbroD-e каждое действие почти всегда является частью более крупного действия, то желательно это отображать в отчете, для более быстрого обнаружения ошибки. Пример атомарного действия с логированием:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; } public void Invoke() { Trace.WriteLine("Opening 'Activity List' page:", "Atomic"); this.Page().Open(); }{/syntaxhighlighter}

Пример метода проверки компонентов страницы с логированием:

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; } public override void ValidateControls() { if (TestExecutionContext.EnableValidationChecks) { Trace.WriteLine("Verify controls on 'Activity List' page:", "Page"); Document.AssertPresentById(this.SearchText.CreateActivityButton); Document.AssertPresentById(this.ImportActivities); Document.AssertPresent(this.SearchText); } }{/syntaxhighlighter}

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

 

{syntaxhighlighter brush: bash;fontsize: 100; first-line: 1; }[C] Login case: [A] Opening 'Login' page: [D] Opening url: 'wp-login.php'. [D] Waiting for page to load. [P] Check page on errors: [D] Check for text is present: 'Error'. [D] Reading page title. Title is: 'Bugs Catcher › Log In'. [P] Assert controls on 'Log In' page: [D] Check for component is present: 'id: user_login'. [D] Check for component is present: 'id: user_pass'. [D] Check for component is present: 'id: wp-submit'. [D] Check for component is present: 'id: rememberme'. [D] Check for component is present: 'link: Lost your password?'. [D] Check for component is present: 'link: ← Back to Bugs Catcher'. [P] Assert text elements on 'Log In' page: [D] Check for text is present: 'Username'. [D] Check for text is present: 'Password'. [D] Check for text is present: 'Remember Me'. [D] Check for text is present: 'Lost your password?'. [D] Check for text is present: '← Back to Bugs Catcher'. [C] Login as 'TestUser', '123456': [A] Filling login credentials: [E] Set 'TestUser' to input field 'TextField [id: user_login]': [D] Typing 'TestUser' in 'TextField [id: user_login]'. [E] Set '123456' to input field 'TextField [id: user_pass]': [D] Typing '123456' in 'TextField [id: user_pass]'. [A] Do Login: [E] Clicking on: 'Button [id: wp-submit]' and wait for page to load: [D] Clicking on: 'Button [id: wp-submit]'. [D] Waiting for page to load. [P] Check is no error message appear: [D] Check for text is present: 'ERROR: Invalid username.'. [D] Check for text is present: 'ERROR: The password you entered for the username'. [P] Check page on errors: [D] Check for text is present: 'Error'. Selenium is stopped{/syntaxhighlighter}

Буквы в квадратных скобках означают уровень фреймворка:

  • [D] – уровень работы с Selenium;
  • [P] – уровень работы со страницей;
  • [E] – уровень работы с элементами;
  • [A] – уровень работы с атомарными действиями;
  • [C] – уровень работы с составными действиями.

4. Подводим итоги

Главные достоинства данного подхода при написании авто-тестов:

  • тесты легко писать. Большее количество людей может писать тесты;
  • тесты понятны и читабельны и, как следствие, их легко поддерживать. К слову, сейчас у нас насчитывается более пятьсот тестов (причем тестовые сценарии достаточно продолжительные), особых проблем с поддержкой и расширением тестов не возникало;
  • из первых двух пунктов вытекает третий – экономия времени и средств;
  • легко добавлять новые тесты, автоматизировать дефекты;
  • тесты гибкие;
  • возможности самого фреймворка легко расширить.

Из минусов можно отметить:

  • Долгий старт. Время на внедрение автоматизации и возврат инвестиций увеличивается;
  • На уровне составных действий в некоторых метах не удалось избежать дублирования кода. Например, для создания и редактирования формы (а это разные составные действия) приходиться местами копировать details step-ы (они фактически всегда одинаковые). Конечно, серьезность проблемы снижает тот факт, что details step-ы состоят из атомарных действий (т.е. правки приходится вносить только в одном месте). Но все равно хотелось бы разрешить данную ситуацию;
  • При не правильном подходе использования ButerbroD-а все его достоинства могут оказаться бесполезными. Например, если все атомарные действия мы засунем в один композит. Нужно с умом относиться к архитектуре составных действий;
  • При разрастании фреймворка для некоторых загруженных сущностей уровень составных действий становится сложно поддерживаемым.
  • Подобного рода «тяжелая автоматизация» больше подходит для долгосрочных проектов.

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

Первые тесты писались достаточно медленно, попутно мы делали некоторые изменения в самом фреймворке и накатывали обертки команд Selenium. Однако довольно быстро скорость разработки тестов значительно выросла. Используя ButerbroD нам удалось быстро заавтоматизировали smoke тесты. После чего мы принялись разрабатывать новые тесты, тем самым расширяя тестовое покрытие продукта. Результаты радовали. К слову, тогда в Easy Projects .NET вносилось много изменений в код, связанных с пересчетом дат. ButerbroD-у тогда сразу удалось зарекомендовать себя верным помощником в борьбе с дефектами.

Главная задача была выполнена, положительный эффект от внедрения автоматизации был очевиден.

Сейчас мы имеет следующие результаты работы тестов:

  • более 500 авто-тестов;
  • 85 тестовых наборов;
  • более 55% покрытия кода;
  • более 80% покрытия требований;
  • приёмочное тестирование не требует людских ресурсов;
  • определенную долю уверенности при выпуске нового релиза.

Продолжение следует!