Статьи

Автоматизированные тесты пользовательского интерфейса

Несколько лет назад я очень скептически относился к автоматическому тестированию пользовательского интерфейса, и этот скептицизм возник из нескольких неудачных попыток. Я написал бы несколько автоматических тестов пользовательского интерфейса для настольных или веб-приложений, а через несколько недель я бы вырвал их из базы кода, потому что стоимость их обслуживания была слишком высока. Поэтому я подумал, что тестирование пользовательского интерфейса было сложным, и, хотя оно дало много преимуществ, было бы лучше свести его к минимуму и тестировать только самые сложные рабочие процессы в системе с помощью пользовательского тестирования, а остальное оставлять для модульных тестов. Я помню, как рассказывал своей команде о пирамиде тестирования Майка Кона и о том , что в типичной системе более 70% тестов должны составлять юнит-тесты, около 5% тестов пользовательского интерфейса и остальные интеграционные тесты.

Поэтому я подумал, что тестирование пользовательского интерфейса было трудным, и хотя оно дало много преимуществ, было бы лучше свести его к минимуму …

Я ошибался! Конечно, тестирование пользовательского интерфейса может быть трудным. Правильное написание тестов пользовательского интерфейса занимает немало времени. Они намного медленнее и более хрупки, чем модульные тесты, потому что они пересекают границы классов и процессов, они попадают в браузер, они включают элементы пользовательского интерфейса (например, HTML, JavaScript), которые постоянно меняются, они затрагивают базу данных, файловую систему и, возможно, сетевые сервисы. Если какая-либо из этих движущихся частей не играет хорошо, у вас сломан тест; но в этом и прелесть тестов пользовательского интерфейса: они тестируют вашу систему полностью. Никакой другой тест не дает вам столько или такой полный охват. Автоматические тесты пользовательского интерфейса, если все сделано правильно, могут быть лучшими элементами в вашем наборе регрессии.

Таким образом, в последних нескольких проектах мои тесты пользовательского интерфейса составляли более 80% моих тестов! Я должен также упомянуть, что эти проекты в основном были CRUD-приложениями с небольшим количеством бизнес-логики, и давайте посмотрим правде в глаза — подавляющее большинство программных проектов попадают в эту категорию. Бизнес-логика все еще должна быть проверена модулем; но остальная часть приложения может быть тщательно протестирована с помощью автоматизации пользовательского интерфейса.


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

Так что же не так и почему? Многие команды начинают автоматизацию пользовательского интерфейса с помощью устройств записи экрана. Если вы делаете веб-автоматизацию с Selenium, вы, скорее всего, использовали Selenium IDE . С домашней страницы Selenium IDE:

Selenium-IDE (интегрированная среда разработки) — это инструмент, который вы используете для разработки тестовых примеров Selenium.

Это на самом деле одна из причин, по которой тестирование пользовательского интерфейса превращается в ужасный опыт: вы загружаете и запускаете экранный рекордер и переходите на свой веб-сайт и нажимаете, нажимаете, печатаете, нажимаете, набираете, вкладываете, печатаете, вкладываете, печатаете, нажимаете и утверждают. Затем вы воспроизводите запись, и она работает. Сладкий!! Таким образом, вы экспортируете действия как тестовый сценарий, вставляете его в свой код, оборачиваете его в тест, выполняете тест и видите, как браузер оживает на ваших глазах, и ваши тесты работают очень гладко. Вы очень взволнованы, делитесь своими результатами с коллегами и демонстрируете их своему боссу, а они очень взволнованы и говорят: « Автоматизируйте ВСЕ »

Через неделю у вас есть 10 автоматических тестов пользовательского интерфейса, и все кажется великолепным. Затем компания просит вас заменить имя пользователя на адрес электронной почты, поскольку это вызвало некоторую путаницу среди пользователей, и вы это делаете. Затем, как и любой другой великий программист, вы запускаете свой набор тестов пользовательского интерфейса, но обнаруживаете, что 90% ваших тестов не работают, потому что для каждого теста вы регистрируете пользователя с именем пользователя, а имя поля меняется, и вам требуется два часа, чтобы заменить все ссылки на username в ваших тестах с email и чтобы тесты снова стали зелеными. То же самое происходит снова и снова, и в какой-то момент вы проводите часы в день, исправляя испорченные тесты: тесты, которые не сломались, потому что что-то пошло не так с вашим кодом; но потому что вы изменили имя поля в вашей базе данных / модели или слегка реструктурировали страницу. Через несколько недель вы прекращаете запускать свои тесты из-за этих огромных затрат на обслуживание и делаете вывод, что тестирование пользовательского интерфейса — отстой.

Вам НЕ следует использовать Selenium IDE или любой другой рекордер экрана для разработки тестовых случаев. Тем не менее, это не сам рекордер экрана, который приводит к хрупкому тестовому набору; это код, который они генерируют, который имеет врожденные проблемы сопровождения. Многие разработчики все еще получают хрупкий набор тестов пользовательского интерфейса, даже не используя устройства записи экрана только потому, что их тесты демонстрируют те же характеристики.

Все тесты в этой статье написаны на веб- сайте Mvc Music Store . У веб-сайта как есть есть некоторые проблемы, которые усложняют тестирование пользовательского интерфейса, поэтому я портировал код и исправил проблемы. Вы можете найти реальный код, против которого я пишу эти тесты, в репозитории GitHub для этой статьи здесь

Так как же выглядит хрупкое испытание? Это выглядит примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class BrittleTest
{
    [Test]
    public void Can_buy_an_Album_when_registered()
    {
        var driver = Host.Instance.Application.Browser;
        driver.Navigate().GoToUrl(driver.Url);
        driver.FindElement(By.LinkText(«Admin»)).Click();
        driver.FindElement(By.LinkText(«Register»)).Click();
        driver.FindElement(By.Id(«UserName»)).Clear();
        driver.FindElement(By.Id(«UserName»)).SendKeys(«HJSimpson»);
        driver.FindElement(By.Id(«Password»)).Clear();
        driver.FindElement(By.Id(«Password»)).SendKeys(«!2345Qwert»);
        driver.FindElement(By.Id(«ConfirmPassword»)).Clear();
        driver.FindElement(By.Id(«ConfirmPassword»)).SendKeys(«!2345Qwert»);
        driver.FindElement(By.CssSelector(«input[type=\»submit\»]»)).Click();
        driver.FindElement(By.LinkText(«Disco»)).Click();
        driver.FindElement(By.CssSelector(«img[alt=\»Le Freak\»]»)).Click();
        driver.FindElement(By.LinkText(«Add to cart»)).Click();
        driver.FindElement(By.LinkText(«Checkout >>»)).Click();
        driver.FindElement(By.Id(«FirstName»)).Clear();
        driver.FindElement(By.Id(«FirstName»)).SendKeys(«Homer»);
        driver.FindElement(By.Id(«LastName»)).Clear();
        driver.FindElement(By.Id(«LastName»)).SendKeys(«Simpson»);
        driver.FindElement(By.Id(«Address»)).Clear();
        driver.FindElement(By.Id(«Address»)).SendKeys(«742 Evergreen Terrace»);
        driver.FindElement(By.Id(«City»)).Clear();
        driver.FindElement(By.Id(«City»)).SendKeys(«Springfield»);
        driver.FindElement(By.Id(«State»)).Clear();
        driver.FindElement(By.Id(«State»)).SendKeys(«Kentucky»);
        driver.FindElement(By.Id(«PostalCode»)).Clear();
        driver.FindElement(By.Id(«PostalCode»)).SendKeys(«123456»);
        driver.FindElement(By.Id(«Country»)).Clear();
        driver.FindElement(By.Id(«Country»)).SendKeys(«United States»);
        driver.FindElement(By.Id(«Phone»)).Clear();
        driver.FindElement(By.Id(«Phone»)).SendKeys(«2341231241»);
        driver.FindElement(By.Id(«Email»)).Clear();
        driver.FindElement(By.Id(«Email»)).SendKeys(«chunkylover53@aol.com»);
        driver.FindElement(By.Id(«PromoCode»)).Clear();
        driver.FindElement(By.Id(«PromoCode»)).SendKeys(«FREE»);
        driver.FindElement(By.CssSelector(«input[type=\»submit\»]»)).Click();
 
        Assert.IsTrue(driver.PageSource.Contains(«Checkout Complete»));
    }
}

Вы можете найти класс BrittleTest здесь .

Host — это статический класс с единственным статическим свойством: Instance , которое при создании экземпляра запускает IIS Express на тестируемом веб-сайте и привязывает Firefox WebDriver к экземпляру браузера. После завершения теста он автоматически закрывает браузер и IIS Express.

Этот тест запускает веб-браузер, переходит на домашнюю страницу веб-сайта Mvc Music Store, регистрирует нового пользователя, просматривает альбом, добавляет его в корзину и оформляет заказ.

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

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

Так что же не так с этим тестом?

  • Это процедурный код. Одной из основных проблем этого стиля кодирования является удобочитаемость или ее отсутствие. Если вы хотите изменить тест или если он сломается из-за изменения одной из задействованных страниц, вам будет сложно понять, что нужно изменить, и провести черту между функциональными разделами; потому что это большая куча кода, где мы получаем «драйвер», чтобы найти элемент на странице и что-то с ним сделать. Нет модульности.
  • Этот тест сам по себе может не иметь большого количества дубликатов, но есть еще несколько подобных тестов, и у вас будет много дублированных селекторов и логики для взаимодействия с веб-страницами из разных тестов. Например, By.Id("UserName") будет продублирован во всех тестах, требующих регистрации, и driver.FindElement(By.Id("UserName")).Clear() и driver.FindElement(By.Id("UserName")).SendKeys(" ") driver.FindElement(By.Id("UserName")).SendKeys(" ") дублируются везде, где вы хотите взаимодействовать с текстовым полем UserName. Затем есть вся форма регистрации, форма для проверки и т. Д., Которые будут повторяться во всех тестах, требующих взаимодействия с ними! Дублированный код приводит к ночных кошмаров о ремонтопригодности.
  • Повсюду много волшебных строк, что опять же является проблемой ремонтопригодности.

Существуют также шаблоны, которые позволяют вам писать более понятные тесты пользовательского интерфейса.

Как и ваш настоящий код, вам придется поддерживать свои тесты. Так что относитесь к ним одинаково.

Что в тестах заставляет нас думать, что мы можем отказаться от качества в них? Во всяком случае, плохой набор тестов, на мой взгляд, гораздо сложнее поддерживать, чем плохой код. У меня были плохие части работающего кода в производстве в течение многих лет, которые никогда не ломались, и мне никогда не приходилось их трогать. Конечно, это было некрасиво и трудно читать и поддерживать, но это работало и не нуждалось в изменениях, поэтому реальные затраты на обслуживание были равны нулю. Ситуация не совсем та же для плохих тестов: потому что плохие тесты сломаются, и исправить их будет сложно. Я не могу сосчитать, сколько раз разработчики избегали тестирования, потому что считают, что написание тестов — огромная трата времени, потому что на его обслуживание уходит слишком много времени.

Тестовый код — это код. Применяете ли вы SRP к своему коду? Тогда вы должны применить его и к своим тестам. Ваш код СУХОЙ ? Затем высушите свои тесты тоже. Если вы не пишете хорошие тесты (пользовательский интерфейс или что-то еще), вы потратите много времени на их обслуживание.

Существуют также шаблоны, которые позволяют вам писать более понятные тесты пользовательского интерфейса. Эти шаблоны не зависят от платформы: я использовал эти самые идеи и шаблоны для написания тестов пользовательского интерфейса для приложений WPF и веб-приложений, написанных на ASP.Net и Ruby on Rails. Таким образом, независимо от вашего технологического стека, вы сможете сделать свои тесты пользовательского интерфейса более удобными, выполнив несколько простых шагов.

Многие из вышеупомянутых проблем коренятся в процедурном характере тестового сценария, и решение легко: объектная ориентация.

Объект страницы — это шаблон, используемый для применения ориентации объекта к тестам пользовательского интерфейса. Из Selenium Wiki :

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

Идея состоит в том, что для каждой страницы в вашем приложении / веб-сайте вы хотите создать один объект страницы. Объекты страницы — это, по сути, эквивалент автоматизации ваших веб-страниц.

Я продвинулся вперед и реорганизовал логику и взаимодействия из BrittleTest в несколько объектов страницы и создал новый тест, который использует их вместо прямого обращения к веб-драйверу. Вы можете найти новый тест здесь . Код скопирован здесь для вашей справки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TestWithPageObject
{
    [Test]
    public void Can_buy_an_Album_when_registered()
    {
        var registerPage = HomePage.Initiate()
            .GoToAdminForAnonymousUser()
            .GoToRegisterPage();
 
        registerPage.Username = «HJSimpson»;
        registerPage.Email = «chunkylover53@aol.com»;
        registerPage.Password = «!2345Qwert»;
        registerPage.ConfirmPassword = «!2345Qwert»;
 
        var shippingPage = registerPage
            .SubmitRegistration()
            .SelectGenreByName(«Disco»)
            .SelectAlbumByName(«Le Freak»)
            .AddToCart()
            .Checkout();
 
        shippingPage.FirstName = «Homer»;
        shippingPage.LastName = «Simpson»;
        shippingPage.Address = «742 Evergreen Terrace»;
        shippingPage.City = «Springfield»;
        shippingPage.State = «Kentucky»;
        shippingPage.PostalCode = «123456»;
        shippingPage.Country = «United States»;
        shippingPage.Phone = «2341231241»;
        shippingPage.Email = «chunkylover53@aol.com»;
        shippingPage.PromoCode = «FREE»;
        var orderPage = shippingPage.SubmitOrder();
        Assert.AreEqual(orderPage.Title, «Checkout Complete»);
    }
}

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

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

1
2
driver.FindElement(By.Id(«Email»)).Clear();
driver.FindElement(By.Id(«Email»)).SendKeys(«chunkylover53@aol.com»);

мы можем написать:

1
registerPage.Email = «chunkylover53@aol.com»;

где registerPage — это экземпляр класса RegisterPage . Флажок на странице становится свойством bool на объекте Page, а отметка и снятие флажка — это просто вопрос установки этого логического свойства в true или false. Аналогично, ссылка на веб-странице становится методом в объекте страницы, и нажатие на ссылку превращает вызов метода в объекте страницы. Так что вместо:

1
driver.FindElement(By.LinkText(«Admin»)).Click();

мы можем написать:

1
homepage.GoToAdminForAnonymousUser();

Фактически, любое действие на нашей веб-странице становится методом в нашем объекте страницы, и в ответ на выполнение этого действия (т.е. вызов метода для объекта страницы) вы получаете экземпляр другого объекта страницы, который указывает на веб-страницу, которую вы только что переход к выполнению действия (например, отправка формы или нажатие на ссылку). Таким образом, вы можете легко связать взаимодействия между представлениями в тестовом скрипте:

1
2
3
4
5
6
var shippingPage = registerPage
           .SubmitRegistration()
           .SelectGenreByName(«Disco»)
           .SelectAlbumByName(«Le Freak»)
           .AddToCart()
           .Checkout();

Здесь, после регистрации пользователя, я попадаю на домашнюю страницу (экземпляр объекта страницы возвращается методом SubmitRegistration ). Поэтому в экземпляре SelectGenreByName я вызываю SelectGenreByName который нажимает на ссылку «Disco» на странице, которая возвращает экземпляр AlbumBrowsePage, а затем на этой странице я вызываю SelectAlbumByName которая нажимает на альбом «Le Freak» и возвращает экземпляр AlbumDetailsPage и т. Д. и так далее.

Я признаю это: много классов для того, что раньше вообще не было классов; но мы получили много преимуществ от этой практики. Во-первых, код больше не является процедурным. У нас есть хорошо продуманная модель тестирования, где каждый объект обеспечивает хорошую инкапсуляцию взаимодействия со страницей. Так, например, если что-то меняется в вашей логике регистрации, единственное место, которое вы должны изменить, это ваш класс RegisterPage вместо того, чтобы проходить весь набор тестов и изменять каждое отдельное взаимодействие с представлением регистрации. Эта модульность также обеспечивает удобство повторного использования: вы можете повторно использовать ShoppingCartPage везде, где вам нужно взаимодействовать с корзиной покупок. Таким образом, в простой практике перехода от процедурного к объектно-ориентированному тестовому коду мы почти устранили три из четырех проблем с первоначальным хрупким тестом, а именно процедурный код, а также дублирование логики и селектора. У нас все еще есть небольшое дублирование, которое мы исправим в ближайшее время.

Как мы на самом деле реализовали эти объекты страницы? Объект страницы в его корне — не что иное, как обертка вокруг ваших взаимодействий со страницей. Здесь я просто извлек взаимодействия с пользовательским интерфейсом наших хрупких тестов и поместил их в свои собственные объекты страницы. Например, логика регистрации была извлечена в свой собственный класс с именем RegisterPage который выглядел так:

01
02
03
04
05
06
07
08
09
10
11
12
public class RegisterPage : Page
{
    public HomePage SubmitRegistration()
    {
        return NavigateTo<HomePage>(By.CssSelector(«input[type=’submit’]»));
    }
 
    public string Username { set { Execute(By.Name(«UserName»), e => { e.Clear(); e.SendKeys(value);});
    public string Email { set { Execute(By.Name(«Email»), e => { e.Clear(); e.SendKeys(value);});
    public string ConfirmPassword { set { Execute(By.Name(«ConfirmPassword»), e => { e.Clear(); e.SendKeys(value);});
    public string Password { set { Execute(By.Name(«Password»), e => { e.Clear(); e.SendKeys(value);});
}

Я создал суперкласс Page который заботится о нескольких вещах, таких как NavigateTo который помогает перейти на новую страницу, выполняя действие, и Execute который выполняет некоторые действия с элементом. Класс Page выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class Page
{
    protected RemoteWebDriver WebDriver
    {
        get { return Host.Instance.WebDriver;
    }
 
    public string Title { get { return WebDriver.Title;
 
    public TPage NavigateTo<TPage>(By by) where TPage:Page, new()
    {
        WebDriver.FindElement(by).Click();
        return Activator.CreateInstance<TPage>();
    }
 
    public void Execute(By by, Action<IWebElement> action)
    {
        var element = WebDriver.FindElement(by);
        action(element);
    }
}

В BrittleTest для взаимодействия с элементом мы выполняли FindElement один раз за действие. Метод Execute , помимо абстрагирования взаимодействия веб-драйвера, обладает дополнительным преимуществом, которое позволяет один раз выбрать элемент, который может быть дорогостоящим, и выполнить несколько действий с ним:

1
2
driver.FindElement(By.Id(«Password»)).Clear();
driver.FindElement(By.Id(«Password»)).SendKeys(«!2345Qwert»);

был заменен на:

1
Execute(By.Name(«Password»), e => { e.Clear(); e.SendKeys(«!2345Qwert»);})

Если мы еще раз посмотрим на объект страницы RegisterPage выше, у нас все еще есть небольшое дублирование. Тестовый код — это код, и мы не хотим дублирования в нашем коде; так что давайте рефакторинг это. Мы можем извлечь код, необходимый для заполнения текстового поля, в метод класса Page и просто вызвать его из объектов страницы. Метод может быть реализован как:

1
2
3
4
5
6
7
8
public void SetText(string elementName, string newText)
{
    Execute(By.Name(elementName), e =>
        {
            e.Clear();
            e.SendKeys(newText);
        } );
}

И теперь свойства в RegisterPage могут быть сокращены до:

1
public string Username { set { SetText(«UserName», value);

Вы могли бы также сделать свободный API для него, чтобы заставить читатель лучше читать (например, Fill("UserName").With(value) ), но я оставлю это вам.

Мы не делаем ничего необычного здесь. Просто рефакторинг нашего тестового кода, как мы всегда делали для нашего, ошибочного, «другого» кода !!

Вы можете увидеть полный код для классов Page и RegisterPage здесь и здесь .

Мы решили процедурные проблемы с помощью хрупкого теста, который сделал тест более читабельным, модульным, DRYer и эффективно поддерживаемым. Есть еще одна проблема, которую мы не исправили: повсюду много волшебных строк. Не совсем кошмар, но все еще проблема, которую мы могли бы исправить. Введите строго типизированные объекты страницы!

Этот подход практичен, если вы используете MV * Framework для своего пользовательского интерфейса. В нашем случае мы используем ASP.Net MVC.

Давайте еще раз посмотрим на RegisterPage :

01
02
03
04
05
06
07
08
09
10
11
12
public class RegisterPage : Page
{
    public HomePage SubmitRegistration()
    {
        return NavigateTo<HomePage>(By.CssSelector(«input[type=’submit’]»));
    }
 
    public string Username { set { SetText(«UserName», value);
    public string Email { set { SetText(«Email», value);
    public string ConfirmPassword { set { SetText(«ConfirmPassword», value);
    public string Password { set { SetText(«Password», value);
}

Эта страница моделирует представление « Регистрация» в нашем веб-приложении (просто скопируйте верхний бит здесь для вашего удобства):

1
2
3
4
5
@model MvcMusicStore.Models.RegisterModel
 
@{
    ViewBag.Title = «Register»;
}

Хм, что это за RegisterModel там? Это модель представления для страницы: M в MVC . Вот код (я удалил атрибуты, чтобы уменьшить шум):

1
2
3
4
5
6
7
public class RegisterModel
{
    public string UserName { get;
    public string Email { get;
    public string Password { get;
    public string ConfirmPassword { get;
}

Это выглядит очень знакомо, не правда ли? Он имеет те же свойства, что и класс RegisterPage что неудивительно, учитывая, что RegisterPage был создан на основе этого представления и модели представления. Давайте посмотрим, сможем ли мы воспользоваться моделями представлений, чтобы упростить объекты нашей страницы.

Я создал новый суперкласс Page ; но общий. Вы можете увидеть код здесь :

1
2
3
4
5
6
7
public class Page<TViewModel> : Page where TViewModel: class, new()
{
    public void FillWith(TViewModel viewModel, IDictionary<Type, Func<object, string>> propertyTypeHandling = null)
    {
      // removed for brevity
    }
}

Класс Page<TViewModel> подклассирует старый класс Page и предоставляет все его функциональные возможности; но у него также есть один дополнительный метод FillWith который заполняет страницу предоставленным экземпляром модели представления! Теперь мой класс RegisterPage выглядит так:

1
2
3
4
5
6
7
8
public class RegisterPage : Page<RegisterModel>
{
    public HomePage CreateValidUser(RegisterModel model)
    {
        FillWith(model);
        return NavigateTo<HomePage>(By.CssSelector(«input[type=’submit’]»));
    }
}

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

После преобразования объектов моей страницы в общие теперь тест выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public class StronglyTypedPageObjectWithComponent
{
    [Test]
    public void Can_buy_an_Album_when_registered()
    {
        var orderedPage = HomePage.Initiate()
            .GoToAdminForAnonymousUser()
            .GoToRegisterPage()
            .CreateValidUser(ObjectMother.CreateRegisterModel())
            .SelectGenreByName(«Disco»)
            .SelectAlbumByName(«Le Freak»)
            .AddAlbumToCart()
            .Checkout()
            .SubmitShippingInfo(ObjectMother.CreateShippingInfo(), «Free»);
 
        Assert.AreEqual(«Checkout Complete», orderedPage.Title);
    }
}

Вот и все — весь тест! Намного более читабельный, СУХОЙ и ремонтопригодный, не так ли?

Класс ObjectMother который я использую в тесте, это Object Mother, который предоставляет тестовые данные (код можно найти здесь ), ничего особенного:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ObjectMother
{
    public static Order CreateShippingInfo()
    {
        var shippingInfo = new Order
        {
            FirstName = «Homer»,
            LastName = «Simpson»,
            Address = «742 Evergreen Terrace»,
            City = «Springfield»,
            State = «Kentucky»,
            PostalCode = «123456»,
            Country = «United States»,
            Phone = «2341231241»,
            Email = «chunkylover53@aol.com»
        };
 
        return shippingInfo;
    }
 
    public static RegisterModel CreateRegisterModel()
    {
        var model = new RegisterModel
        {
            UserName = «HJSimpson»,
            Email = «chunkylover53@aol.com»,
            Password = «!2345Qwert»,
            ConfirmPassword = «!2345Qwert»
        };
 
        return model;
    }
}

Некоторые веб-страницы очень большие и сложные. Ранее я говорил, что тестовый код — это код, и мы должны рассматривать его как таковой. Обычно мы разбиваем большие и сложные веб-страницы на более мелкие, а в некоторых случаях повторно используемые (частичные) компоненты. Это позволяет нам создавать веб-страницы из более мелких, более управляемых компонентов. Мы должны сделать то же самое для наших тестов. Для этого мы можем использовать Page Components.

Компонент страницы во многом похож на объект страницы: это класс, который инкапсулирует взаимодействие с некоторыми элементами на странице. Разница в том, что он взаимодействует с небольшой частью веб-страницы: он моделирует пользовательский элемент управления или частичное представление, если хотите. Хорошим примером для компонента страницы является строка меню. Панель меню обычно появляется на всех страницах веб-приложения. Вы действительно не хотите повторять код, необходимый для взаимодействия с меню в каждом объекте страницы. Вместо этого вы можете создать компонент страницы меню и использовать его из объектов вашей страницы. Вы также можете использовать компоненты страницы для работы с сетками данных на своих страницах, а для дальнейшего продвижения сам компонент страницы сетки может состоять из компонентов страницы строки сетки. В случае Mvc Music Store мы могли бы иметь TopMenuComponent и SideMenuComponent и использовать их с нашей TopMenuComponent SideMenuComponent .

Как и в вашем веб-приложении, вы также можете создать, скажем, LayoutPage страницы LayoutPage который моделирует макет / главную страницу, и использовать его в качестве суперкласса для всех остальных объектов вашей страницы. Страница макета будет состоять из компонентов страницы меню, чтобы все страницы могли попадать в меню. Я полагаю, что хорошим правилом было бы иметь компонент страницы для частичного просмотра, объект страницы макета для макета и объект страницы для веб-страницы. Таким образом, вы знаете, что ваш тестовый код так же гранулярен и хорошо составлен, как и ваш код.

То, что я показал выше, было очень простым и надуманным образцом с несколькими вспомогательными классами в качестве инфраструктуры для тестов. На самом деле требования к тестированию пользовательского интерфейса намного сложнее: существуют сложные элементы управления и взаимодействия, вы должны писать и читать со своих страниц, иметь дело с задержками в сети и иметь контроль над AJAX и другими взаимодействиями Javascript, нужно запускать разные браузеры и так далее, о которых я не объяснил в этой статье. Хотя можно обойти все это, использование некоторых фреймворков может сэкономить вам много времени. Вот рамки, которые я очень рекомендую:

Рамки для .Net:

  • Seleno — это проект с открытым исходным кодом от TestStack, который помогает вам создавать автоматизированные тесты пользовательского интерфейса с Selenium. Он фокусируется на использовании объектов страницы и компонентов страницы, а также на чтении и записи на веб-страницах с использованием строго типизированных моделей представления. Если вам понравилось то, что я сделал в этой статье, то вам также понравится Seleno, так как большая часть кода, показанного здесь, была заимствована из базы кода Seleno.
  • White — это платформа с открытым исходным кодом от TestStack для автоматизации клиентских приложений на основе платформ Win32, WinForms, WPF, Silverlight и SWT (Java).

Раскрытие информации: я являюсь соучредителем и членом команды разработчиков в организации TestStack.

Каркасы для Ruby:

  • Capybara — это платформа для приемочного тестирования веб-приложений, которая помогает вам тестировать веб-приложения, имитируя взаимодействие реального пользователя с вашим приложением.
  • Полтергейст — водитель для Капибары. Это позволяет вам запускать ваши тесты Capybara в браузере WebKit без головы, предоставляемом PhantomJS .
  • page-object (я лично не использовал этот драгоценный камень) — это простой драгоценный камень, который помогает создавать гибкие объекты страниц для тестирования приложений на основе браузера. Цель состоит в том, чтобы упростить создание слоев абстракции в ваших тестах, чтобы отделить тесты от элемента, который они тестируют, и обеспечить простой интерфейс для элементов на странице. Работает как с watir-webdriver, так и селен-webdriver.

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

Если вы хотите взять один пункт из этой статьи, это должно быть: Тестовый код — это код. Если вы подумаете об этом, все, что я сделал в этой статье, это применил хорошие методы кодирования и объектно-ориентированные методы, которые вы уже знаете, к тесту пользовательского интерфейса.

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

Счастливого тестирования!