Какой должна быть идеальная структура проекта автотестов?

Здравстуйте!

Хочу узнать о хороших вариантах архитекры проекта автоматизированных тестов.

На данный вопрос натолкнула статья: How To Structure Your Test Code

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

Два основные подхода:

  1. Один тест кейс на класс
  2. Используй наследование с умом

Стркуткра проекта при таком подходе выходит следующая:

  1. Базовый класс для тестов (содержит общие аспекты для всех тестов, такие как запуск браузера)
  2. Один или более промежуточных тест класов (содержат общие методы для группы тестов)
  3. Сам тест класс (в котором тесты запускаются)

Картинка с визуализацией:

Код базового класса:

 class Test {
   protected setUpBeforeTestClass(){
       // initialize a browser driver, connect to servers
   }

  protected setUpBeforeTestMethod() {
   // initialize testPage
   // login to the app, if necessary
   }

  protected tearDownAfterTestMethod() {
   // logout of the app, if necessary
  }

  protected tearDownAfterTestClass() {
   // close connections, close browser as needed
  }
}

Код промежуточного класса:

class PostTest extends Test {

  protected setUpBeforeTestClass(){
        // no changes needed
        super.setUpBeforeTestClass();
  }

  protected setUpBeforeTestMethod() {
       // do the parent actions, then add some post-specific actions
       super.setUpBeforeTestMethod();
       testPage.goToPostPage();
   }

   protected tearDownAfterTestMethod() {
       // logout of the app, if necessary
   }

   protected tearDownAfterTestClass() {
         // close connections, close browser as needed
   }
}

И наконец код класса с тестом:

class CreateNewPostTest extends PostTest {

    public testCreateValidNewPost(){
        // test is ready to go!
        testPage.createNewPost(postDetails);
        assert testPage.isPostPresent(postDetails);
    }

    public testCreateInvalidNewPost() {
          // test is ready to go!
         testPage.createNewPost(invalidPostDetails);
          assert !testPage.isPostPresent(invalidPostDetails);
    }

 // and so on
}

Что кажется плохим в данной архитектуре:

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

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

Лично я в данный момент использую следующую архитектуру:

  1. Много тестовых классов, которые отвечают за открытие браузера, его закрытие и за запуск тестов. Если это тест с несколькими проверками (например, покупка билета), то основные действия выполняются при инициализации класса (pre-condition), а каждая отдельная проверка (сами тесты) выполняются в отедльных методах этого класса.
    Если же тест содержит только одну проверку (как тест успешной авторизации), то данные тесты группируются в одном классе (например, тест класс для авторизации у меня содержит 2 теста - успешная и неуспешная авторизация).

  2. Классы с общими действиями. Эти классы не наследуются, просто их объекты создаются в классах с тестами. Наприер, класс с общими действиями по покупке билета содержит методы: “Добавить билет в корзину”, “Изменить кол-во билетов”, “Оплатить билет”; тогда все эти методы вызываются в тестовых классах, что позвляет избежать дублирования кода.

  3. PageObjects - классы в которых реализована логика взаимодействия со страницами сайта. Методы этих классов используются для классах с общими действиями, а также в тестовых классах.

В моем подходе в минусы можно занести:

  1. Дублирование кода запуска и закрытия браузера
  2. Все остальное что вы напишите в комментариях :grinning:

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

4 лайка

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

Еще есть принцип “Предпочитай композицию наследованию”. Скорее всего, не имелось в виду прятать общие шаги в надклассы. А 2 уровня происходят именно от того, что тесты разбиваются по файлам: там, где был бы класс с 7 тестами (группа тестов с одинаковыми пред- и постусловиями), и он был бы производный от базового, они делают 7 классов по 1 тесту, а общие пред- и пост-условия тестов одной группы выносим в промеждуточный уровень.

Это не супер. Код, отвечающий за открытие браузера, должен быть в одном месте. Лучше использовать фабрику сессий.

Поддерживаю.

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

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

https://github.com/robotframework/Selenium2Library/tree/master/src/Selenium2Library/utils
Это отвечает принципу ограничения ответственности класса.
Так же следует поступать и с инициализацией других ресурсов, например, базы данных.

Да, например, сами тесты)). Просто заранее спланировать фреймворк никогда не получается. Берем то, что есть, и повторяющиеся вещи выносим “за скобки”, если это действительно одинаковые действия.
Если много специфичного кода, надо найти золотую середину. Идеальных схем тут нет, т.к. идеальные схемы - это крайности: можно переборщить с абстракцией и тогда одна правка может сломать много зависимостей, а можно погрязнуть в китай-коде, где одно и то же в 100 мест скопировано, и во всех 100 местах наросли специфичные правки, так что объединить уже не получится.

1 лайк

А возможно ли применить фабрику, если у меня класс с тестами является Generic?

То есть, он выглядит так:

       [TestFixture(typeof(ChromeDriver))]
       public class PagesTests <TWebDriver> where TWebDriver : IWebDriver, new()
       {
                [Test]
                public void My_Test(){}
       }

Я использую C# + NUnit 3

В чем проблема: в таком случае мой класс фабрики тоже должен быть Generic, и в таком случае при его инициализации мне нужно будет передавать в него параметры. Чтобы передать параметры, я долежн обозначить его как [TestFixture]. [SetUpFixture] не принимает параметров. Мне кажется что такая архитектура будет работать неправильно или не будет работать вообще.

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

Скажите, прав ли я?
В самом первом комментарии темы один из гл. подходов называется Один тест кейс на класс. Далее приводится пример класса CreateNewPostTest, где реализовано два метода, а именно testCreateValidNewPost и testCreateInvalidNewPost. Разве каждый из данных методов не является тест кейсом? А если так, то тут получается Один тест-сьют на класс, а не Один тест-кейс на класс. Или я не прав?

Мне кажется в этой области нет ничего такого идеального. Есть только свод правил, которыми надо манипулировать, я его немного под себя подогнал :slight_smile: делюсь:

  1. весь код должен быть переиспользован, а не копипаститься

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

  3. поскольку мы не пишем в классе странице методы по работе с ней, то надо создать прослойку, ее можно назвать хелпером или как-то иначе, пример - LoginHelper, NavHelper. В данных классах будут реализованы методы по работе со страницами, фичами…

  4. для ожиданий, методов (оберток) для экшенов на страницах (клик, выбор из выпадающего списка, ввод в поле, и многое другое)… Все эти методы уходят в свои классы, пример: WaitUtils, UiUtils

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

  6. класс BaseTest, от которого наследуются все тесты - должен быть только один, в котором все должно быть просто. Там лежит общая логика перед всеми тестами и после всех тестов. ИМХО: лучше туда никогда не складывать, методы по типу setUpBeforeTestMethod(), это можно положить в класс с тестом, потому что зачастую оно очень уникально и если для 100 тестов подойдет, то для остальных пяти - может не подойти… У меня там лежит что-то такое:

     @Listeners({ScreenshotListener.class, TestListener.class})
     public class BaseTest {
       
       private static final Logger logger = LogManager.getLogger(BaseTest.class);
       
       protected ApplicationManager app;
       
       @BeforeClass(alwaysRun = true)
       public void setUp() {
           app = new ApplicationManager();
           // или какой-то:
           initDriver();
       }
        
       @AfterClass(enabled = true)
       public void afterClass() {
           // something
       }
        
       @AfterSuite(alwaysRun = true)
       public static void createAllureProperties() {
           AllureProperties.create();
       }
       
       @AfterSuite(alwaysRun = true)
       public void tearDown() {
           app.stop();
       }
        
     }
    
  7. должно быть грамматное логгирование и отчеты. Для отчетов использую Allure

  8. должна быть реализована многопоточность, даже если тестов сейчас 5 штук

  9. должны открывать браузеры: chrome, firefox

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

  11. на драйвер и вейт должны быть сделаны геттеры, чтобы безболезненно можно было вызвать драйвер или вейт, в любом из классов

  12. мне кажеться, что один кейс на один класс - как-то избыточно. Если я проверяю к примеру логин, то я его проверяю в одном классе и тесты там собраны на валидные и не валидные данные, для разных логинов в систему, из разных мест. Объединяя все это в одну группу…

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

7 лайков

Вам надо прекращать кодить на C# по вечерам…
А вообще, с чего такая любовь к “дескрипторной” модели данных? Не оверхед ли это для тестов?

Никогда не кодил на C#…

Это плохо? А как должно быть, по вашему? :slight_smile:

Не совсем понимаю понятие - “дескрипторной” модели данных :slight_smile:

Мне кажется это малость не умесно в данном контексте, концептуально все слишком усложнено.
Что вам дадут эти гет/сет методы? Какую вы логику собрались вкладывать педж-обжект? А если страница состоит из множества виждетов, ваш класс превратится в огромную кашицу из методов. Ваша логика должна быть выражена в тестах как раз, а пейдж обжект только РЕАЛИЗОВЫВАТЬ методы по работе с этой страницей, не более.
Самый простой, и надежный метод реализации, мне кажется:
BacePage с переданным драйвером, и наследованым от Utils где есть методы по работе с елементами, и дальше просто композиция классов, где каждая страница класс, каждый виджет атрибут страницы, если изменение страницы влечет переход на другу - возвращаем класс, иначе используем атрибуты.
На базе этой модели пишем тесты, при чтении которых сразу становистя понятно что за чем идет,
Сами тесты компонуем как удобно, или как вам диктует ваш лаунчер - классы модлуи функции, это все личное, для наборов тестов юзаем фикстуры, написанные из ранее созданной логики.
Естественно, если код предполагает так какой то научный интерес, можно поизгаляться как угодно, в случае простоты мне кажется мой вей - симпл изи))
ЗЫ. пишу на питоне, может у вас на Java все так сложно надо делать?

2 лайка

Думаю это именно проблема Джавы.

Я на питоне делаю примерно все так же как вы описали.

В джаве тоже было нужно один раз использовать геттеры/сеттеры для элементов, потому как библиотека BeansUtils для рефлекшена ожидает наличие таких. Но в один день мне попалась страница с кучей элементов, и я трезвым утренним взглядом глянул на декларацию страницы - и ужаснулся.
2 дня редезайна по удалении этой ненужной чепухи - и все стало красиво и на круги своя :slight_smile:

В питоне все НАМНОГО проще и удобнее. И тесты структурировать, и сортировать по фичам/стори. Фикстуры это вообше прелесть неземная, я такую гибкую зависимость никогда на Джаве бы не сделал как делаю сейчас.

Геттеры и сеттеры для элементов пейджей вообще не нужны. Тесты должны оперировать исключительно бизнес логикой приложения. Геттеры могут понадобиться разве что для получения состояния компонентов / текста и т.п. И то, только ради ассертов. Сами элементы отдавать нельзя, если уж вы следуете концепции инкапсуляции. А все почему? Потому что отдавая ссылку на элементы через геттеры, вы по сути делаете ваши объекты mutable. И толку тогда от private филдов, если кто угодно из-вне может посредством геттера изменить их состояние? Исключением разве что могут быть final филды. Но опять-таки, такой трюк не прокатит, если вы инициализируете элементы где-то вне страницы, при помощи фабрик / рефлекшена и т.п.

не, ну я одно время как то пытался юзать property в питоне, но оно оказалось просто не нужно, как и “приватизация”, максимум что делаю это согласно pep8 делаю подчеркивание…но это максимум.
Пытался использовать множественное наследование, в против композиции, но приходится ОЧЕНЬ много планировать, а малейшая ошибка в названии метода (два одинаковых в не нужных местах) делали свое дело - отдебажить было ой как не просто.
В итоге пришел к парадигме модели, которая со стороны драйвера помещается в глобал фикструру, а со стороны тестов использует атр/методы модели и из них компонует тесты…из за особой читаемости даже отказался от степ-описаний - сигнатура все сама за себя говорит.
Единственно есть не приятные вещи:

  1. При написании тестов отсутствует авто-дополнение методов в IDE, решается последовательной разработкой: написал модуль - пиши тесты. (где то было решение, но уже не помню)
  2. Редко, но бывает, приходится определить многовато фикстур, но, если вы делаете модуль на страницу, то такое практически отсутствует
  3. Бывают структуры сайтов которые плохо компонуются (очень много форм, фильтров, виджетов, и лендинги), но если опять таки грамотно разделить то становится по проще - множественное наследование там бы ставило крест.
    Плюсы.
  4. При композиции, очень легко встраивать доп плюшки, для уменьшения тестов. пример, для каждой инициализации страницы я пытаюсь искать уникальный елемент для данной странцы, который в не удачном случае завалит тест. Помолго против проверок “А на этой ли я странице”, “а открылся ли виджет календаря”, и другие похожие. Обычно это делается ифами и множественными асертами (фи-фи-фи такими быть)
  5. Так как по сути, элементы композиции ничем не связаны кроме самой пейджи, вполне разрешается хранить локаторы как атрибуты, или даже (!!) хранить прям в методах, главное страницы именуйте логически верно.
  6. Из за минимизации наследования, очень легко рефакторить модель, к примеру одно время я писал свой тул-класс для работы с таблицами и ютюьом, что вылилось в выпиливание хардкода и простой заменой на имя метода тулзы, нет наследования - нет проблем с методами типа open/click и прочее.
  7. Красота.:grinning:

ЗЫ Добавлию, в планах написания некой библиотеки, для декларативного написния тестов, согласно концептуальной идеи модели приложения. пока только набор тул-классов реализующих работу с кнопками/таблицами/редакторами(пока один)

Вы имеете ввиду, что есть, к примеру страница LoginPage, в ней есть куча элементов, а ниже идут методы по работе с этими элементами? Что-то типо такого в тесте получается?

LoginPage loginPage = init(LoginPage.class);
PopUp popUp = loginPage.open("/login.html").then().loginWith(name, pass).thenGoTo(PopUp.class);
assertThat(popUp.getTitleMessages()).isEqualTo("11111!");
assertThat(popUp.getBodyMessages()).isEqualTo("22222");

И никаких прослоек, работаешь тупо со страницей и каждый метод (действие) может возвращать новую страницу или новый блок какой-то, поп-ап?

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

вот именно так и было изначально у меня в первой версии…

  1. Редко, но бывает, приходится определить многовато фикстур, но, если вы делаете модуль на страницу, то такое практически отсутствует

Ничего страшного в определении фикстур нет. Главное понимать цель и предназначение. У меня используються все виды scope фикстур, что помогает очень грамотно разделить нужную реализацию функционала и переиспользовать во всех местах без дубликации.

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

Все верно, главное правильно разделять.

Плюсы.

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

У меня тоже декларация страницы включает в себя некий web_page_id элемент, который я использую для вызова страницы. Правда у меня еще обязательно присутствует invoke_actions метод, который есть абстрактный в базовом класе WebPage, который используеться для вызова страницы. Т.е. реализована цепная логика вызова страницы.

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

Храню все локаторы внутри страницы, а все элементы класифицированы по типу, кнопка/дроп даун лист/текст филд и т.д. Все красиво, наглядно и легко читабельно.

ЗЫ Добавлию, в планах написания некой библиотеки, для декларативного написния тестов, согласно концептуальной идеи модели приложения. пока только набор тул-классов реализующих работу с кнопками/таблицами/редакторами(пока один)

Все это лучше и описывать в спицифических классах. У меня например это сделано в модуле ДатаДайпс :slight_smile:

А что если в следующем тесте вам нада залогиниться опять что бы выполнить тест на второй странице?

Как вы его напишете?

В смысле? Идешь и вызываешь хелпер в котором описан логин (авторизация) на страницу… В любом тесте и месте можешь вызвать и залогиниться или разлогиниться.

Вот как это было раньше (когда еще не делал хелперы, а методы все лежали в классе страницы, рядышком с элементами и для элементов небыло геттеров, они просто были приватными):

//Тест 1
LoginPage loginPage = init(LoginPage.class);
PopUp popUp = loginPage.open("/login.html").then().loginWith(name, pass).thenGoTo(PopUp.class);
assertThat(popUp.getTitleMessages()).isEqualTo("11111!");
assertThat(popUp.getBodyMessages()).isEqualTo("22222");

//Тест 2
LoginPage loginPage = init(LoginPage.class);
loginPage.open("/login.html").then().loginWith(name, pass);
assertThat(loginPage.getInvalidSignInMessages()).isEqualTo("The email was not found.");

Теперь пытаюсь таким подходом свести к хелперу в любом месте…

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

Ну как-то так. Ведь PageObject и является представлением реальной веб-страницы, с которой вы можете осуществлять какие-то операции.

Но только в тесте не нужно хранить ссылки на пейджи. Зачем?

1 лайк

Да так и было, и подход нравился, пока не накрутил себе на голове новое решение и решил переписать с прослойками :slight_smile:

ссылки на пейджи - вот это имете ввиду?

LoginPage loginPage = init(LoginPage.class); 

Вообще незачем, но пока не решил вопрос, как избавиться от этого в своем фреймворке… Опыта не так много еще, но избавлюсь :slight_smile: