Разделы портала

Онлайн-тренинги

.
Gradle для тестировщика
04.09.2023 00:00

Автор: Насибуллин Ирек, Ростелеком Информационные Технологии

Меня зовут Ирек, и я в профессиональном IT с 2012 года. Прошел путь от специалиста службы поддержки до разработчика. На данный момент занимаюсь автоматизацией тестирования в компании РТК ИТ.

В статье хочу рассказать о полезных для автоматизатора возможностях Gradle.

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

Всё уже есть в документации

Действительно у Gradle отличная документация и там можно найти абсолютно все, что вам нужно, но осилить целиком возможно будет трудновато, так как полная документация на момент написания статьи занимает 1357 страниц.

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

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

С чего все начинается?

При создании нового проекта build.gradle выглядит следующим образом.

plugins {
    id 'java'
}

group = 'org.example'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
}

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

#1 Делаем вывод в консоль более информативным

Если запустить прогон тестов командой

./gradlew test

то увидим следующее сообщение

> Task :test FAILED

Test3 > test3() FAILED
    org.opentest4j.AssertionFailedError at Test3.java:8

3 tests completed, 1 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///home/eriknas/projects/habr/build/reports/tests/test/index.html

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1s
3 actionable tasks: 2 executed, 1 up-to-date

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

Добавляем следующие строчки в блок test{} в build.gradle

test {
    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
        exceptionFormat "full"
    }
    useJUnitPlatform()
}

и смотрим, как поменялся вывод

Starting a Gradle Daemon, 1 incompatible and 1 stopped Daemons could not be reused, use --status for details

> Task :test FAILED

Test3 > test3() FAILED
    org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
        at app//org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
        at app//org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
        at app//org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
        at app//org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
        at app//org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:31)
        at app//org.junit.jupiter.api.Assertions.assertTrue(Assertions.java:180)
        at app//Test3.test3(Test3.java:8)

Test1 > test1() PASSED

Test2 > test2() PASSED

3 tests completed, 1 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///home/eriknas/projects/habr/build/reports/tests/test/index.html

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 4s
3 actionable tasks: 1 executed, 2 up-to-date

Теперь у нас есть вывод о том, какие тесты и в каком порядке выполнялись. Это очень удобно, когда мы смотрим все это в выводах пайплайна.

#2 Перезапускаем упавшие тесты

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

plugins {
    id 'java'
    id "org.gradle.test-retry" version "1.5.3"
}

Далее указываем количество попыток перезапуска тестов

test {
    retry {
        maxRetries = 3
    }
    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
        exceptionFormat "full"
    }
    useJUnitPlatform()
}

Запустим тесты и видим следующее (часть вывода сокращу, для облегчения чтения)

Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details

> Task :test FAILED

Test3 > test3() FAILED
    ...

Test1 > test1() PASSED

Test2 > test2() PASSED

Test3 > test3() FAILED
    ...

Test3 > test3() FAILED
    ...

Test3 > test3() FAILED
    ...

6 tests completed, 4 failed

FAILURE: Build failed with an exception.

...

BUILD FAILED in 6s
3 actionable tasks: 3 executed

Видим, что test3 упал, потом прошли оставшиеся тесты и далее было 3 попытки перезапустить test3. Если хотя бы одна из них завершилась бы удачно, то тест бы считался успешным.

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

#3 Настраиваем выборочный запуск тестов

Добавим в конец файла build.gradle

/ Задача для запуска тестов с тегом "smoke"
tasks.register('smoke', Test) {
    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
        exceptionFormat "full"
    }
    useJUnitPlatform {
        includeTags("smoke")
    }
}

// Задача для запуска тестов с тегом "regress"
tasks.register('regress', Test) {
    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"
        exceptionFormat "full"
    }
    useJUnitPlatform {
        includeTags("regress")
    }
}

Отлично, теперь у нас есть 2 новые задачи по тестированию, которые умеет запускать Gradle. Для примера методу test3 добавим аннотации Tag("smoke") и Tag("regress"). А методу test1 только Tag("regress").

Запустим

./gradlew smoke

И видим, что выполнился только test3

> Task :smoke FAILED

Test3 > test3() FAILED
...

1 test completed, 1 failed

...

BUILD FAILED in 948ms
3 actionable tasks: 1 executed, 2 up-to-date

Теперь запустим регресс

$ ./gradlew regress

> Task :regress FAILED

Test1 > test1() PASSED

Test3 > test3() FAILED
...

2 tests completed, 1 failed

...

BUILD FAILED in 906ms
3 actionable tasks: 1 executed, 2 up-to-date

Очень удобная штука. Еще можно передавать значения необходимых тэгов прямо из CI, чтобы например запустить все тесты которые например относятся к версии 2.1.0 и не придется хардкодить номер версии вашего приложения.

#4 Запускаем выборочно тесты без предварительной настройки

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

Например

./gradlew test --tests SomeTestClass

Выполнит все тесты, описанные в классе SomeTestClass.

Можно задать несколько параметров для запуска

./gradlew test --tests SomeTestClass1 --tests SomeTestClass2

Подробно это описано в документации.

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

tasks.register('test-by-tag', Test) {
    useJUnitPlatform {
        if (project.hasProperty("includedTags")) {
            includeTags(project.properties.get("includedTags").toString())
        }
        if (project.hasProperty("excludedTags")) {
            excludeTags(project.properties.get("excludedTags").toString())
        }
    }
}

Теперь можно запускать тесты с фильтрами по тэгам.
Например

./gradlew test-by-tag -PincludedTags=smoke

-P это ключ для обозначения параметра. Подробнее тут.

#5 Запускаем все отключенные тесты

Представьте, что у нас есть некоторое количество тестов, которые помечены аннотацией Disabled и мы хотим проверить, что они все еще поломаны. Комментировать или удалять аннотации в коде будет очень утомительно.

Но мы можем задать в системных свойствах значение, которое отключит аннотацию Disabled.

test {
    systemProperties = [
            "junit.jupiter.conditions.deactivate" : "org.junit.*DisabledCondition"
    ]
    useJUnitPlatform()
}

Можно создать отдельную задачу, чтобы время от времени запускать и проверять отключенные тесты. Подробнее, как это работает, описано здесь.

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

#6 Отключаем запуск тестов на конкретном стенде

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

groovy
tasks.register('db', Test) {
    onlyIf {
        System.getenv("STAND") != "Production"
    }
    useJUnitPlatform {
        includeTags("db")
    }
}

Теперь, если мы запустим эту таску на всех стендах, то она отработает везде, кроме продакшена.

Подведем итоги

Конечно, это только малая часть возможностей. Есть еще множество плагинов, которые могут помочь побороть сложности на вашем проекте.

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

Если кто-то хочет разобраться с Gradle детальнее, то есть хорошая серия статей на эту тему.

На этом все. 

Обсудить в форуме