Из .NET на Java вместе с Swd.Starter (J)

В целях демонстрации работы моей утилиты SWD Page Recorder, которая умеет записывать и генерировать код классов PageObject, я отдельно создал проект SWD.Starter.

Это проект – стартовый набор для собственного фреймворка, в котором я постарался реализовать те хорошие практики автоматизации, которые сам использую в работе:

  1. Начинать автоматизацию UI нужно с PageObject
  2. Для каждого Page Object должен быть написан смоук-тест, который открывает страницу и проверяет что актуальные контролы соответствуют декларациям PageObject-класса.
  3. Браузер инициализируется автоматически, при первом обращении и автоматически переедаться через тесты без закрытия, ведь именно так работает реальный человек.
  4. Для указания типа браузера (Firefox, IE, Chrome…) используется файл конфигурации
  5. Браузер закрывается автоматически, если он был открыт, по завершению всех тестов.
  6. PageObject автоматически вызывает инициализируется PageFactory при вызове конструктора по умолчанию.
  7. Используется система документации кода (Doxygen), которая мотивирует хорошо документировать код и получить в итоге красивую документацию в формате HTML.

И всё это уже работает на .NET! Теперь пришла очередь переписать SWD.Starter на Java.

Я уже начал это делать, но… хэх… учитывая то, что я только изучаю Java и смотрю на нее с колокольни .NET/C#… я скорее всего могу допустить ошибки и всякие глупости, которые для C# считаются нормальным кодом, а для Java – как раз глупым.

Прошу форумчан помочь в ревью кода и подходов.

Код проекта можно найти тут
dzharii/Swd.StarterJ

А если вы установите расширение Octotree то можно будет и удобно посмотреть всю структуру папок и кода на гихабе.

Это было длинное вступление. А что я уже реализовал и как я напишу в следующих постах ниже.

(to be continued…)

4 лайка

Окружение

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

Кроме того, я заметил, что каждая IDE создаёт проект в собственном формате… это очень странно, но хорошо что есть Maven, который поддерживается NetBeans из коробки.

Так что:

  • Тип проекта: Maven
  • IDE: NetBeans
  • Java 8
  • Unit Test Framework: TestNG

Структура проекта

  • org.swd.starterj.core будет содержать базовые классы PageObject, методы для работы с WebDriver, чтение конфигурации и .т.д

  • org.swd.starterj.demo – это проект, содержащий тесты для демонстрации

  • org.swd.starterj.demo.testmodel – бизнес-логика и классы PageObject для демо-проекта.

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

(to be continued…)

Чтение конфигурации

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

Для .NET я привык использовать файл app.config. Как оказалось, в Java также есть стандартные механизмы работы с файлами конфигурации.

Для чтения есть класс java.util.Properties, ну а файл конфигурации я назвал config.properties и поместил в корень проекта.

Так выглядит код чтения параметров из класса Config

    public static String readProperty(String propertyName ) throws IOException {
        Properties prop = new Properties();
        String result = null;
        
        try (FileInputStream input = new FileInputStream("config.properties"))
        {
            prop.load(input);
            result = prop.getProperty(propertyName);
        }
        catch(IOException e)
        {
            throw e;
        }
        return result;
    }

Пока что кэш не реализован. Весь файл конфигурации перечитывается с диска при каждом обращении. Добавил пункт на счёт кэша себе в TODO.

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

        String url = Config.applicationMainUrl();
        driver.navigate().to(url);

А сам файл конфигурации (“config.properties”) выглядит так:

# (!) Application URL
# ------------------- 
applicationMainUrl=http://swd-tools.com

# WebDriver Settings
# ------------------
# Remote or Local?
swdIsRemote=false

# Remote hub/node URL: 
swdRemoteUrl=http://127.0.0.1:4444/wd/hub


# Browser Name (type)
swdBrowserType=Firefox
# swdBrowserType=InternetExplorer
# swdBrowserType=Chrome
# swdBrowserType=PhantomJS
# … 
# swdBrowserType=Android

Автоматическое создание и закрытие браузера

В пакете org.swd.starterj.core этим занимаются два класса:

SwdBrowser – это фасад для работы с текущем окном браузера. Предназначен для того, чтобы предоставить упрощённое API и скрыть ненужные технические детали.

Самый главный метод пока, это SwdBrowser.getDriver(), который либо создаст браузер на основе текущих данных из конфигурационного файла, либо вернёт уже созданный.

Вся работа по созданию нового браузера будет сделана классом WebDriverRunner, который сам по себе Синглтон… но пораждает новый драйвер, а потому он фабрика. Короче ВебдрайверСинглтоноФабрика… брр…

Вот как выглядит создание нового браузера в WebDriverRunner.getDriver()

   public WebDriver getDriver() throws MalformedURLException, IOException {
        if (driver == null) {
            // TODO: Implement browser creation from 
            // configuration file
            driver = RunDriver(Config.swdBrowserType(), Config.swdIsRemote(), Config.swdRemoteUrl());
        }
        return driver;
    }

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

    private void addShutDownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                if (driver != null) {
                    driver.quit();
                    driver = null;
                }
            }
        });
    }

Ну, и напоследок…

Документация кода

Doxygen – замечательный генератор документации. Достаточно потратить вечер и разобраться с настройкой Doxyfile – и вы получите красивую HTML документацию.

Но, в нашем случае, уже все настроено. Достаточно запустить doxygen_run.cmd и документация на основе JavaDoc будет сгенерирована в корне, в папке docs.

Это мотивирует документировать код комментариями.

Кода в итоге, получилось немного, но вот что уже что готово:

  1. Начинать автоматизацию UI нужно с PageObject
  2. Для каждого Page Object должен быть написан смоук-тест, который открывает страницу и проверяет что актуальные контролы соответствуют декларациям PageObject-класса.
  3. Браузер инициализируется автоматически, при первом обращении и автоматически переедаться через тесты без закрытия, ведь именно так работает реальный человек.
  4. Для указания типа браузера (Firefox, IE, Chrome…) используется файл конфигурации
  5. Браузер закрывается автоматически, если он был открыт, по завершению всех тестов.
  6. PageObject автоматически вызывает инициализируется PageFactory при вызове конструктора по умолчанию.
  7. Используется система документации кода (Doxygen), которая мотивирует хорошо документировать код и получить в итоге красивую документацию в формате HTML.

И тут у меня вопрос:

Есть ли способ автоматически выкачать chromedriver и iedriver при первом билде проекта?
Как можно ли подключить оба эти бинарника в Maven зависимости?
Как сделать так, чтобы под Windows и Linux (для хромдрайвера) качались бинарники соответствующие платформе?

Если у вас есть замечания и предложения, не стесняйтесь, пишите!

Буду здесь отписывать пожелания по улучшению.

  1. Не советовал бы стартовать с 8й джавы. Еще не все библиотеки на нее перешли. К примеру, сонар аналайзеры не умеют пока работать с байт кодом восьмерки. AOP компайлеры - аналогично. Люди могут отказываться от вашего продукта только из-за несовместимости с существующими компонентами. Да, в семерке нет лямбд, но на текущем этапе они и не нужны. Можно будет плавно к ним перейти, когда 8я джава более или менее закрепится.
  2. Я бы поместил проперти файлы в стандартный resources каталог (можно подкаталог этого каталога), дабы не захламлять рут. Если вы компилятору скормите проперти расширение в качестве resource pattern, то не нужно будет указывать абсолютный путь (как и в случае с рутом). Делается это в настройках.
  3. Касательно чтения самих пропертей. Я бы порекомендовал посмотреть в сторону apache.commons.configuration пакета. Там даже есть возможность миксовать различные виды пропертей: систем, файлы и т.п.
  4. Раз вы взялись за статику, то драйвер лучше сразу поместить в ThreadLocal контейнер, что избавит вас от головной боли в случае внедрения масштабирования.
  5. По синтаксису: константы должны быть в uppercase с ‘_’ разделителем; классы должны быть в camel case; имена методов - mixed case (первая маленькая, дальше camel), в .Net с большой, знаю - сам долго привыкал. :smile:
  6. Utility классы (такие, как Config) нужно делать еще и final, помимо private пустого конструктора. Кстати, input аргументы методов тоже желательно делать final.
  7. Для любителей засовывать в тесты несколько верификейшенов, можно сразу предусмотреть soft asserts.
  8. Есть еще некоторые мелкие замечания, но не столь существенные.

А почему NetBeans? Очень рекомендую IntelliJ IDEA Community Edition. Она абсолютно бесплатная даже для коммерческих целей. Уверяю, что после нее больше ничего другого не захочется. И некоторые мелочи, о которых я говорил выше, она сама вам подсветит и предложит исправить. К тому же, там встроенная интеграция всего - maven, ant, все репозитории и т.п. Идеальный инструмент. Хотя, ее интеллисенсу до решарпера конечно еще далеко, но все же, ничего лучше я не встречал для java.

Еще очень рекомендую развернуть локально Sonar. Вам понадобится только qube и runner. Он вам сделает базовый код ревью с приблизительной оценкой technical debt. У нас сейчас всех девов заставляют делать пре-коммит анализ сонаром, помимо общего процесса код ревью. Очень экономит время.

П.С. Если еще что-то найду, дам знать.

1 лайк

Спасибо, @ArtOfLife,

  1. По поводу Java 8, очень правильные аргументы, но тут я подчеркну, что использую Runtime 1.8. Лямбды у меня, например, по умолчанию в коде отключены. Я так понимаю, что сейчас пишу код, совместимый с JRE7. Тем не менее, учитывая все аргументы, решил что буду двигаться в сторону Java 8 сейчас.

  2. Проперти файлы: Спасибо, возьму на заметку и попробую. Я пока запускаю через IDE, и в этом случае абсолютный путь не требуется, но когда буду тестировать запуск из командной строки, то вернусь к этому вопросу.

  3. apache.commons.configuration – пока что я по возможности пытаюсь использовать стандартные библиотеки. Особенность Starter Kit в том, что он идёт не как бибилиотека, а как уже каркас проекта, в котором можно со временем просто переписать отдельные модули или заточить их под свои нужны.

  4. ThreadLocal: это обязательно возьму на заметку. В мире .NET работать с многопоточными тестами сложнее, чем, как я слышал, в Java. Я придерживаюсь того, что для паралельного выполнения тестов, их лучше запустить в отдельных процессах, а не потоках. Но, если эта фича с многопоточностью действиельно часто используется в TestNG + WebDriver, я обязательно уделю этому внимание.

“5,6”: Поправлю в самое ближайшее время. Стараюсь соблюдать эти правила, но пока проскакивает :smiley:

“7.” SoftAssert – блин, классная штука

Если честно, то IntelliJ IDEA я ещё просто не пробовал. В принципе, после Eclipse, от которого я плевался, я попробовал NetBeans, где работать стало намного удобней и понятней.

Пока что все там устраивает. Да, там тоже есть статический анализ кода с подсказаками как исправить и советует когда какой import сделать.

@ArtOfLife еще раз спасибо за очень полезные комментарии.

Эти драйвера являются по сути хаками над браузерами. Когда webdriver официально признают в качестве стандарта, тогда и пропадет весь этот геморрой с подключением. Я уверен, что бинарники из соображений безопасности никогда не станут заливать в репозиторий. Можно конечно их засовывать в jar. Но в любом случае запустить exe из jar без распаковки не получится. Т.е. в плане простоты подхода, для начинающих будет гораздо легче просто скачать .exe и прописать к нему путь в коде или через path. Другой момент связан с RemoteWebDriver. Ноду то в любом случае понадобится бинарник именно на машине, где будут подниматься браузеры, а не собираться приложение. Т.е. универсальный вариант вряд ли получится придумать. А если и получится, то времени и сил на это уйдет гораздо больше, нежели полученного профита.

Да, похоже, этот момент со скачиванием екзешников нужно будет просто документировать.
Там есть еще одна фишка, в том, что если екзешник драйвера находится в рабочей папке проекта, то его не нужно добавлять в PATH.

Я планирую документировать то, что бинарники нужно вначале скачать в отдельную папку webdrivers
А затем, уже через Maven копировать все содержимое папки в рабочую папку тестов.
По крайней мере, так я делал в Вижуалстудии :smiley:

Что-то вы с архитектурой намудрили. Зачем нужен WebDriverRunner? Ведь по сути SWDBrowser+WebDriverRunner = SWDBrowserManager.
Даже если разделять сущности для старта браузера и работы с ним, то задача раннера просто отдать драйвер менеджеру. А у вас синглетон на синглетоне и синглетоном погоняет ))

Спасибо, @vmaximv, этот момент на счёт объединения классов, я пока оставлю как есть, так как оба этих класса ещё будут расти в количестве методов, но потом, когда работа над ними будет завершена, обязательно вспомню об этом пожелании при рефакторинге.

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

Структура папок и имена классов

В данный момент, в проекте есть один библиотечный пакет:

org.swd.starterj.core

и один тестовый проект «Demo»
Я сразу же разделил его на пакет тестов: org.swd.starterj.demo

и пакет бизнес-логики, где, в том числе, будут хранится декларации PageObjects. Это org.swd.starterj.demo.testmodel

Для тестирование одного приложения, назовём его “Demo”, тестовых пакетов может быть много: org.swd.starterj.demo.smoke, org.swd.starterj.demo.acceptance, org.swd.starterj.demo.somefeaturename, но все они используют ресурсы из Тестовой Модели org.swd.starterj.demo.testmodel

Имена классов

Кстати, и тестовых проектов, таких как «Demo» может быть несколько. И у каждого будет своя тестовая модель и свои пакеты тестов.

В пакете «Core» я храню все то универсальное, что имеет отношение только к вебдрайверу, но не имеет отношения к конкретным проектам: универсальные базовые классы, интерфейсы, методы-помощники.

В Тестовой Модели для конкретного проекта, я обязательно переопределяю базовый класс и называю его MyBasePage

Если что-то что начинается с префикса My..., то это означает, что оно специфично для конкретного тестового проекта. В данном случае, MyBasePage – это базовый класс для проекта “Demo”. Если вдруг я захочу добавить ещё один тестовый проект, назовём его “ATInfoTests”, то в нем также будет присутствовать своя тестовая модель, а в ней класс с именем MyBasePage

Я сам вижу что не совсем хорошо это расписал… но пока не могу придумать лучшего текста :smiley:

Автоматический вызов PageFactory.initElements

Я начал работу с поддержкой пейджобжектов. Добавил новый пакет org.swd.starterj.core.pageobjects с базовым классом CorePage и интерфейсом SelfTestingPage

Так же, добавил в Demo-проект свой базовый класс для Пейджобжектов:
public abstract class MyBasePage extends CorePage implements SelfTestingPage

Кода в CorePage не много, но вот что он делает…

public abstract class CorePage {
    
    public WebDriver getDriver()
    {
        return SwdBrowser.getDriver();
    }
    
    public CorePage()
    {
        PageFactory.initElements(getDriver(), this);
    }
    
}

Инициализация элементов дочернего PageObject класса проводится посредством вызова PageFactory.initElements из конструктора самого базового класса.

В C# и Java есть такая особенность, что если есть классы с конструкторами без параметров:
C → B → A

То конструктор базового класса A будет автоматически вызван при создании класса C.
Я обычно не передаю никаких значений в конструкторы PageObject, но если понадобится – то вызов необходимых методов уже нужно будет разрулить руками.

Использование интерфейса SelfTestingPage можно наблюдать в смоук тестах в файле:
Smoke_test_for_each_pageobject.java

    public void testPage(MyBasePage page)
    {
        page.invoke();
        page.verifyExpectedElementsAreDisplayed();
    }
    
    @Test
    public void test_EmptyPage() 
    {
        testPage(MyPages.getEmptyPage());
    }

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

А в самой странице (SomeRealPagе) , реализовать три метода: invoke(), isDisplayed() и verifyExpectedElementsAreDisplayed()

Сам же набор смоук тестов откроет каждую страницу при помощи invoke(), а дальше вызовет метод для само тестирования PageObject класса – verifyExpectedElementsAreDisplayed().

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

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

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

Методы-расширения для веб-элементов

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

SomePage.someElement.WaitUntilVisible(1000).Click();

Компилятор, конечно же, в последствии преобразует этот код в что-то на подобии:

WebElementExtensions.WaitUntilVisible(SomePage.someElement, 1000).Click();

И, похоже, что подобным вызовом мне прийдётся пользоваться на Java.

В недавних изменениях, я добавил отдельный класс WebElementExtensions в котором и будут хранится расширения. Их планируется не много, может быть остановлюсь на тех, которые уже есть.

А уже есть:

  • waitUntilVisible – который дожидается пока элемент появится или выбросить TimeoutException
  • getElementText – который умеет корректно получать значения у тегов input, select и других
  • isDisplayedSafe – который возвращает true когда элемент видимый и не выбрасывает исключений в случае отсутствия элемента в DOM страницы.

В CorePage, я добавил метод with(), который делает работу с «расширениями» более синтаксически сладкой:

public abstract class CorePage {
    
    public WebDriver getDriver()
    {
        return SwdBrowser.getDriver();
    }

    /**
     * 
     * @param sourceElement: Webdriver PageObject element
     * @return 
     */
    public WebElementExtensions with(WebElement sourceElement)
    {
        return new WebElementExtensions(sourceElement);
    }
    
    public CorePage()
    {
        PageFactory.initElements(getDriver(), this);
    }
}

Я планирую, что внутри любого пейджобжекта, наследующего CorePage, можно будет написать что-то вроде:

with( btnLogin ).waitUntilVisible(1000).click();

В принципе, это самое важное, что я хотел сделать.

Теперь нужно взять напильник, и прикрутить возможность в Page Recorder записи пейджобжектов и смоук тестов для StarterJ.

В java-api практически все вейты реализованы “в коробке”, и нет нужды их реализовывать заново:

http://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/support/ui/ExpectedConditions.html

http://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/support/ui/WebDriverWait.html

Спасибо, @vmaximv,

Принято. Сейчас я поправил код так, чтобы использовался стандартный wait вебдрайвера

WebElementExtensions:

    public WebElement waitUntilVisible(long timeOutMilliSeconds) throws Throwable {
        WebDriverWait wait = new WebDriverWait(SwdBrowser.getDriver(), timeOutMilliSeconds);
        
        wait.pollingEvery( 100, TimeUnit.MILLISECONDS )
            .ignoring( NoSuchElementException.class, StaleElementReferenceException.class );
        
        ExpectedCondition elementVisibleCondition = ExpectedConditions.visibilityOf(sourceWebElement);
        return (WebElement) wait.until(elementVisibleCondition);
    }

нужно будет ещё тесты написать, чтобы убедится что все работает как задумано.

MyPages – точка доступа к страницам

Вначале, хочу напомнить, что все имена классов с префиксом My означают, что они отвечают за нечто специфичное в контексте текущего тестового проекта.

В одном репозитории может быть несколько проектов, например ProjectA и ProjectB. И каждый из них будет иметь свой MyPages. Но, мы знаем, что если MyPages, используемый в тестах для ProjectA будет возвращать только страницы ProjectA.

Собственно, MyPages содержит в себе набор фабричных методов, которые порождают задекларированные страницы PageObject классов.

По сути, это замена вызову конструкторов. И… наверное, это покажется вначале странным, что я рекомендую (рекомендую, а не запрещаю) использовать следующую строку для получения экземпляра PageObject:

LoginPage loginPage = MyPages.getLoginPage();

вместо:

LoginPage loginPage = new LoginPage();

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

В IDE, после того как вы поставите точку, будет показан список уже готовых PageObject классов. Вы сможете быстрее найти нужную страницу в списке.

MyPages.getLoginPage() на самом деле вызовет внутренний метод getPage(LoginPage.class), который вы можете переписать для решения многих задач:

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

  • По сути, это место для любых хаков связанных с созданием пейджобжекта

  • К объекту страницы можно прозрачно применить AOP

В моей практике было приложение, состоящие из огромного количества фреймов. Тогда я ввёл интерфейс FormSwitchable с методом SwitchToFrame() в самом PageObject. В MyPages SwitchToFrame() вызывался автоматически при получении страницы. Это было очень удобно и экономило много строк кода.
Вот код текущей реализации MyPages:

public class MyPages {
    
    public static <T extends MyBasePage> T getPage(Class<T> pageObjectClass)
    {
        T newInstance = null;
        
        try {
            newInstance = pageObjectClass.newInstance();
        } catch (InstantiationException | IllegalAccessException ex) {
            Logger.getLogger(MyPages.class.getName()).log(Level.SEVERE, null, ex);
        }
        return  newInstance;
    }
    
    public static final EmptyPage getEmptyPage()
    {
        return getPage(EmptyPage.class);
    }
    
    public static final TwitterLoginPage getTwitterLoginPage () { 
        return getPage(TwitterLoginPage.class); 
    }
    
    
    // Put your new pages here:
}

Мои и не мои страницы

Еще одно свойство MyPages в том, что это очень помогает при реализации взаимодействия между проектами.

Ну, например, у нас есть некоторое внешние приложение ProjectA, в котором зарегистрированный пользователь создаёт новую сущность, какие-то новые данные.

Допустим, ProjectA реализуют некоторый сложный механизм добавления данных, частично обрабатывает их на клиенте и сервере и работает только в Internet Explorer и не предоставляет никакого API… так что корректно добавить новую сущность можно только через UI.

Наш проект ProjectB должен отобразить сущность, добавленную в ProjectA, по сути, в коде теста будут использоваться пейджобжекты и из ProjectA и из ProjectB… ну да… и в том и в том проекте будут такие классы с одинаковыми именами как LoginPage, MainPage… и тому подобное. Это неизбежно приведёт к путанице.

Но, используя подход с MyPages можно избавится от этой путаницы.

Допустим в ProjectA и ProjectB есть классы MyPages и мы пишем тесты для ProjectB.

Тогда, в ProjectB мы создаём класс ProjectAPages, который наследуется от ProjectA.MyPages, тем самым создавая синоним для страниц ProjectA.

В тесте для ProjectB теперь мы пишем что-то вроде:

ProjectAPages.getDataFormPage().saveForm(“item1”);
SomeData importedData =  MyPages.getDataFeedPage().getFirstItem();
…

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

Всё-таки, Idea удобнее :smile: