Привет, Хабр! На связи Марк Ерофеев и Никита Потапов из команды Platform V UI Workflow СберТеха. Мы затронем одну из наименее раскрытых тем — тестировании плагинов IDE. Если вы хотя бы раз пытались протестировать плагин, то знаете, что примеров с хорошим покрытием тестами днём с огнём не найти. Плагины либо не тестируются вовсе, либо логика их настолько проста, что хватает элементарной проверки функциональности.

Мы расскажем, как автоматизировали тестирование плагина для IntelliJ IDEA. Статья будет полезна всем, кто ищет информацию на эту специфическую тему или вообще интересуется нетривиальными задачами в области тестирования.

Что мы тестировали и зачем

Мы разрабатываем Platform V UI Workflow — фреймворк для управления навигационными сценариями во фронтальных приложениях. Он поставляется в составе server-side приложений, которые обслуживают работу Сбербанк Онлайн. С фреймворком работают около 150 команд разработки Сбера. Чтобы упростить им жизнь, мы написали плагин для IntelliJ IDEA с функциями подсветки синтаксиса, навигации по компонентам, сниппетами кодовых конструкций и т. д. 

Ещё при проектировании плагина мы подумали: как тестировать решение, которым будет пользоваться столько команд, и которое, скорее всего, быстро обрастёт новыми функциями? Unit-компонентные тесты со временем не смогут обеспечить полного покрытия, а регрессионное тестирование каждого релиза будет долгим и дорогим. Хорошо бы автоматизировать процесс, причём так, чтобы всё тестирование укладывалось в 2-3 часа, а результат не уступал бы качеству ручных проверок. Стоимость разработки такого решения точно окупилась бы в будущем за счёт «бесплатных» запусков и высокой частоты релизов. Да и просто хотелось найти неочевидное и изящное решение задачи, с которой никто из нас до этого не сталкивался.

Особенности тестирования IntelliJ IDEA

Большинство тестов на платформе IntelliJ — функциональные тесты на основе модели, а значит:

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

  • Тесты проверяют функцию в целом, а не отдельные функции, составляющие её реализацию.

  • Тесты не проверяют пользовательский интерфейс Swing, а работают непосредственно с базовой моделью.

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

Тестирование плагина относится к тестированию на основе модели. Изначально у нас была модель на основе взаимодействия разработчика с UI плагина и средой разработки IntelliJ Idea. Мы подобрали несколько библиотек, которые используются для тестирования open source-решений, и использовали их, чтобы получить такую схему:

Selenium-like подход к тестированию решений

После запуска плагина под gradle task runIdeForUiTests получили запущенный экземпляр IntelliJ IDEA одновременно с robot-server-plugin и развёрнутым в нём нашим продуктом. К экземпляру можно обратиться по адресу http://127.0.0.1:8082.

Для того, чтобы составлять, находить и проверять локаторы элементов UI, используется Xpath. На рисунке ниже — пример дерева элементов плагина. В нашем случае полностью отображён welcome frame плагина — это первая страница при запуске IntelliJ IDEA:

Итак, библиотека подключена к проекту, тестовый клиент разработан. Теперь нужно решить вопрос, что конкретно тестировать? Без тестового окружения не получится воспроизвести функциональность плагина. Поэтому решили создать синтетическое приложение со всеми компонентами функциональности плагина как модель. А по ней написать тестовые сценарии продукта и разделить функциональность на тестовые наборы (test-suite).

Основные Swing-компоненты, которые мы выделили в IntelliJ IDEA, — это имеющиеся фикстуры из библиотеки remote-fixtures:

  • Dialogs (DialogFixture) — все возможные всплывающие окна (прим. Project Structure).

  • Editor (com.intellij.remoterobot.fixtures.TextEditorFixture) — окно редактора кода.

  • ProjectTree (com.intellij.remoterobot.fixtures.JTreeFixture) — дерево проекта, каталог файлов.

  • Buttons (com.intellij.remoterobot.fixtures.JButtonFixture) — все воpможные кнопки.

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

Вот пример вызова фикстуры на Java:

BaseTest.java     
@org.junit.Test
    public void test(){
        RemoteRobot remoteRobot = new RemoteRobot("http://127.0.0.1:8082");
        JButtonFixture mavenJButton = remoteRobot.find(JButtonFixture.class, byXpath("//div[@class='StripeButton' and @text='Maven']"));
        List<JButtonFixture> jButtonFixtureList = remoteRobot.findAll(JButtonFixture.class, byXpath("//div[@class='StripeButton']"));
    }

А вот — на Kotlin:

BaseTest.kt 
    @Test
    fun test(remoteRobot: RemoteRobot) = with(remoteRobot) {
        val remoteRobot: RemoteRobot = RemoteRobot("http://127.0.0.1:8082")
        val mavenButton = remoteRobot.find<JButtonFixture>(byXpath("//div[@class='StripeButton' and @text='Maven']"))
        val jButtonList = remoteRobot.findAll<JButtonFixture>(byXpath("//div[@class='StripeButton']"))
    }

Тестовая среда и модели

Для всех Swing-компонентов функциональности плагина мы разработали симуляционные элементы в синтетическом приложении. Но остался ряд открытых вопросов. Например, как понять, что у нас сконфигурировалось всё окружение IDEA, синтетическое приложение собрано и запущено, зависимости загрузились, и плагин будет работать с этой синтетикой?

Мы спроектировали BaseTest и расширили набор фикстур так, чтобы они задействовали стандартные Swing-компоненты IDEA. А ещё настроили все окружения для запуска тестового набора (test-suite) функциональности плагина. Ниже листинг нашего BaseTest-примера:

BaseTest.kt 
@ExtendWith(RemoteRobotExtension::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ProjectBaseTest {
    @BeforeAll
    fun setupProject(remoteRobot: RemoteRobot) = with(remoteRobot) {

        // Открываем проект
        welcomeFrame { openProject("TestProject") }

        
        // Ждём, пока загрузиться IDEA
        waitFor(Duration.ofMinutes(4), Duration.ofSeconds(10),
            "The idea don't open project in 4 minutes.") {
            hasAnyComponent(byXpath("//div[@class='IdeFrameImpl']"))
        }
        idea {
            // Ждём, пока IDEA всё проиндексирует после открытия проекта
            ideStatusLoader { waitUntilAllBgTasksFinish(5) }

            // Конфигурируем jdk проекта
            projectStructure {
                openProjectStructure()
                    .setProjectJdk("11  java version \"11.0.16\"")
                    .applyAndOk()
            }

            // Ждём завершения индексации IDEA после установки новых параметров проекта
            ideStatusLoader { waitUntilAllBgTasksFinish(5) }

            // Открываем Maven, инсталлируем и закрываем панель
            mavenPanel {
                openMavenPanel()
                    .runMavenCommand("clean install")
                    .closeMavenPanel()
            }

            // Ждём, пока Maven всё соберёт
            buildTerminalConsole {
                waitUntilBuildHasFinished(5)
            }
        }
    }
}

Далее рассмотрим основные компоненты, задействованные в BaseTest:

Элемент загрузки процессов IntelliJ IDEA.
Элемент загрузки процессов IntelliJ IDEA.

Фикстура, представленная ниже, помогает завершить проверку и дождаться, пока IntelliJ IDEA загрузит и проанализирует всё в этом элементе:

IdeStatusLoader.kt 
fun RemoteRobot.ideStatusLoader(function: IdeStatusLoaderFixture.() -> Unit) {
    find<IdeStatusLoaderFixture>().apply(function)
}

@DefaultXpath(by = "IdeStatusBarImpl type", xpath = "//div[@class='IdeStatusBarImpl']")
@FixtureName(name = "Ide Status Bar") 
class IdeStatusLoaderFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) {

    private fun progressPanel(): ComponentFixture {
        return find(byXpath("//div[@class='InlineProgressPanel']"))
    }

    fun waitUntilAllTasksFinish(durationOfMinutes: Long = 15) {
        waitFor(Duration.ofMinutes(durationOfMinutes),
            Duration.ofSeconds(10),
            "IDEA background tasks not finish in $durationOfMinutes minutes.") { checkProgressPanel() }
    }

    private fun checkProgressPanel(): Boolean {
        for (i in 1..7) {
            if (progressPanel().findAllText().isNotEmpty()) {
                return false
            }
        }
        return true
    }
}
Maven-панель IntelliJ IDEA.
Maven-панель IntelliJ IDEA.

Следующая фикстура задействует Maven-панель:

MavenPanel.kt 
fun RemoteRobot.mavenPanel(function: MavenPanelFixture.() -> Unit) {
    find<MavenPanelFixture>().apply(function)
}

@FixtureName("MavenProjectFixture")
class MavenPanelFixture (remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : IdeaFrame(remoteRobot, remoteComponent){
    fun openMavenPanel() : MavenPanelFixture{
        button(byXpath("//div[contains(@tooltiptext.key, 'inspection.group') and contains(@visible_text, 'Maven')]")).click()
        return this
    }

    fun runMavenCommand(command: String = "clean install") : MavenPanelFixture{
        button(byXpath("//div[@myaction.key='action.Maven.ExecuteGoal.text']")).click()

        keyboard {
            enterText(command)
            enter()
        }

        waitFor(Duration.ofMinutes(1), Duration.ofSeconds(3),
            "The build console don't open in 1 minutes.") {
            hasAnyComponent(byXpath("//div[@accessiblename='Editor']"))
        }
        return this
    }
    fun closeMavenPanel(){
        if (hasAnyComponent(byXpath("//div[@class='MavenProjectsNavigatorPanel']"))) {
            button(byXpath("//div[contains(@tooltiptext.key, 'inspection.group') and contains(@visible_text, 'Maven')]")).click()
        }
    }
}
Окно загрузчика run-команд Maven.
Окно загрузчика run-команд Maven.

Эта фиксатура помогает дождаться, пока подтянутся все зависимости Maven и соберётся проект:

BuildTerminalConsole.kt 
fun RemoteRobot.buildTerminalConsole(function: BuildTerminalConsoleFixture.() -> Unit) {
    find<BuildTerminalConsoleFixture>().apply(function)
}

@DefaultXpath(by = "Build Terminal Console", xpath = "//div[@class='BuildView']")
@FixtureName(name = "Build Terminal Console Panel")
class BuildTerminalConsoleFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) {

    private fun buildConsoleText(): TextEditorFixture {
        return textEditor(byXpath("//div[@accessiblename='Editor']"), Duration.ofSeconds(2))
    }

    private fun isBuildSuccessful(): Boolean {
        return buildConsoleText().findAllText().stream().anyMatch { it.text.contains("BUILD SUCCESS") }
    }

    fun waitUntilBuildHasFinished(duration: Long = 9) {
        waitFor(Duration.ofMinutes(duration),
            Duration.ofSeconds(3),
            "IDEA console build did not finish in $duration minutes.") { isBuildSuccessful() }
    }
}

Контейнеризация в Docker

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

  • Любой образ Ubuntu с JDK — мы использовали adoptopenjdk/openjdk11 с открытым портом 8082.

  • Maven.

  • Пакеты X11vnc и Xvbf.

Xvbf — cервер отображения, реализующий одноимённый протокол сервера X11. В отличие от других аналогичных серверов Xvfb выполняет все графические операции в виртуальной памяти без отображения выходных данных на экране. С помощью X11vnc можно настроить доступ с удалённого клиента к компьютеру, на котором выполняется сеанс X Window и программное обеспечение X11vnc, и управлять своим рабочим столом X11 (KDE, GNOME, Xfce и т. д.) с удалённого компьютера в собственной сети пользователя или через Интернет, как если бы пользователь сидел перед ним.

Для развёртывания GUI в пакете Xvbf мы используем xfvbf-run с различными опциями запуска IDEA с robot-server. Вот пример, как это сделать:

startup.sh 
/bin/bash xvfb-run ./gradlew runIdeForUiTest

Дальше, настроив VNC-серверы, можем подключатся к удалённому рабочему столу-контейнеру. Впрочем, можно обойтись и без этого режима отладки — просто с помощью команды xvbf-run поднять IDEA с robot-server. 

Всё, можно начинать тестирование. 

Тестирование плагинов: полезная информация

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

В целом, тестирование плагинов IDE — специфическая тема. Информации об этом очень мало, но нам удалось собрать небольшую библиотеку. Надеемся, что материалы, которые помогли спроектировать наш проект, будут вам полезны:

  1. IntelliJ Platform Plugin SDK - Testing Overview 

  2. Тестирование на основе моделей

  3. Ссылка на фреймворк

  4. Пакет Xvbf 

  5. Пакет X11vnc

  6. Фреймворк jetBrains/jetCheck

Комментарии (0)