Когда я писал свой первый gradle-плагин, я проверял его работоспособность следующим образом:
Опубликовал версию
n
в plugins.gradle.orgПроверил опубликованный плагин вручную на тестовом проекте
Нашел ошибку/доработал, увеличил версию
n=n+1
, затем снова пункт 1
Такой вот PDD (Publish Driven Development). Сегодня поговорим о том, как писать эффективные тесты на собственные gradle плагины.
Сложности тестирования
Почему в начале я тестировал gradle-плагин именно через публикацию? Потому что я не знал, как тестировать иначе. Я не знал, как опубликовать gradle-плагин в maven local repository. Я не знал, как написать unit-тест для gradle-плагина. Поэтому я пошел самым легким путем. Тем, что наверняка сработает. Это, кстати, одна из причин, почему люди не пишут unit-тесты: они не знают, как это делать. В случае с gradle-плагином сложностей добавляет тот факт, что gradle - не самая простая система сборки. Обычно на вход подается некоторая часть build-скрипта, а на выходе какое-то действие, усложняющее работу build-скрипта. Повторить и проверить то же самое в unit-тестах сходу сложно.
Берешь и пишешь тест
Хорош ныть. Берешь и пишешь, чего сложного то. Какая там сигнатура у основного метода плагина?
@Override
public void apply(final Project project) {
То есть нужно просто подготовить объект project
, передать его в метод apply
, затем проверить, все ли верно отработало в этом apply
. Тогда тестовый метод будет выглядеть следующим образом:
@Test
public void someSimpleTest() {
Project project = Mockito.spy(Project.class);
Mockito.when(project.getExtensions()).thenReturn(...);
Mockito.when(project. ...).thenReturn(...);
...
new MyPlugin().apply(project);
Mockito.verify(project, Mockito.times(1)).getExtensions();
Mockito.verify(project). ... ;
...
}
Вот и все. Можно написать еще парочку подобных и нет проблем, да? Но проблемы есть. Тут используются моки. Такой тест будет всегда падать, если реализацию метода apply
менять без изменения поведения плагина. Подробнее о том, почему тестирование с использованием моков - это плохо - раз, два.
Специальная подготовка проекта
Кто немножечко углублялся в тему тестирования gradle-плагинов, наверняка сталкивался с классом org.gradle.testfixtures.ProjectBuilder. Он позволяет подготовить project
более естественным для плагина путем. Без тонких знаний реализации метода apply
вашего плагина. В самом примитивном варианте можно сделать вот так:
Project project = ProjectBuilder.builder().build()
и этого будет достаточно, чтобы дальше применять ваш плагин и проверять, как он отработал. Можно будет через полученный экземпляр project
проверить, были ли созданы ожидаемые Task
-и или Extension
-ы, например. Или даже, если ваш плагин зависит от project.afterEvaluate(...)
, то можно будет запустить evaluate
проекта прямо в тесте с помощью:
project.evaluationDependsOn(":");
Такой подход позволяет более гибко и независимо от реализации apply
настроить проект перед проверкой вашего плагина. Простой пример:
@Test
fun configurePluginWithEmptyExtension() {
val projectFolder = tmpFolder.newFolder()
val project = ProjectBuilder.builder()
.withProjectDir(projectFolder)
.build()
project.pluginManager.apply("com.nikialeksey.arspell")
project.evaluationDependsOn(":")
MatcherAssert.assertThat(
project.extensions.getByType(ArspellExtension::class.java),
IsNull.notNullValue()
)
MatcherAssert.assertThat(
project.tasks.getByName("arspell"),
IsNull.notNullValue()
)
}
Проверки из реальной жизни
После написания unit-тестов на gradle-плагин все равно остается соблазн проверить плагин в работе вручную “на всякий” сразу после публикации. Потому что часто вместе с плагином идет какой-нибудь хитрый DSL (Domain Specific Language) у его Extensions, который с одной стороны нужно правильно реализовать, и с другой у него есть несколько способов быть вызванным. Кроме того, ваш плагин может быть использован в gradle-скриптах написанных на разных языках: kotlin или groovy. В groovy может быть использованы различные способы настройки extension
-ов:
myPlugin {
myArgument = "myValue"
}
или так:
myPlugin.myArgument = "myValue"
или даже так (это, конечно, зависит от реализации extension
-а):
myPlugin {
setMyArgument("myValue")
}
Все это должно работать (по крайней мере то, что вы ожидаете). Проверить такое в вышеупомянутых unit-тестах не получится. Но автоматизировать подобные проверки очень хочется. Не проверять же, в конце концов, каждый раз это руками.
Composite builds
На помощь к нам приходят composite builds. Они позволяют использовать локально один gradle-проект в другом gradle-проекте без публикации в какие-либо репозитории. Поэтому предлагаю следующее: под каждую проверку плагина из “реальной жизни” (в скрипте kotlin, в скрипте на groovy, в скрипте с разным использованием одного и того же extension
) подготовить отдельный gradle-проект, и в каждый из этих тестовых gradle-проектов включить проект, содержащий наш тестируемый плагин. Вот так это будет выглядеть в файловой системе:
- myPlugin/ <!-- Our plugin main module -->
- myPlugin-module-1/ <!-- Our plugin additional module -->
- myPlugin-module-2/ <!-- Our plugin additional module -->
- myPlugin-tests/ <!-- The plugin integration tests -->
- kts/ <!-- For kts scripts -->
- build.gradle.kts
- settings.gradle.kts <!-- Here we should include our plugin -->
- groovy/ <!-- For groovy scripts -->
- build.gradle
- settings.gradle <!-- Here we should include our plugin -->
- build.gradle
- settings.gradle
Файлы ./myPlugin-tests/kts/settings.gradle.kts
, ./myPlugin-tests/groovy/settings.gradle
выглядят так:
includeBuild("../../") // include main plugin module
Теперь достаточно проверить, что build
проектов ./myPlugin-tests/kts
и ./myPlugin-tests/groovy
прошел успешно, или то, что запускается какая-нибудь задача вашего плагина. Это будет означать, что скрипты с различным использованием extension
-ов вашего плагина хотя бы компилируются/интерпретируются, и, вероятно, передают необходимые входные данные в плагин. Такие проверки можно написать в виде bash
-скриптов или используя org.gradle.testkit.runner.GradleRunner, но я выбрал более простой путь - .github/workflows
(тут будет пример из реального проекта, поэтому вот его исходник):
jobs:
build:
steps:
- name: Build with Gradle
run: ./gradlew check
- name: Check arspell plugin usage with groovy
working-directory: ./arspell-plugin-gradle-test/groovy/
run: ./gradlew arspell
- name: Check arspell plugin usage with kts
working-directory: ./arspell-plugin-gradle-test/kts/
run: ./gradlew arspell
Параметр working-directory помог выполнить нужные проверки без лишних сложностей с cd.
Пожалуй, на этом можно закончить. Описанные подходы по тестированию gradle плагинов я успешно применяю в тестировании своих небольших проектах, исходники которых вы можете найти на GitHub: arspell, porflavor.
Комментарии (3)
js605451
06.01.2022 18:45+3Project project = Mockito.spy(Project.class); Mockito.when(project.getExtensions()).thenReturn(...); Mockito.when(project. ...).thenReturn(...);
Делать моки на чужой код - плохая идея. Вы не контролируете как тот код работает, поэтому делать высказывания вида "когда - тогда" - это как в таких розовых очках ходить, и убеждать всех, что всё хорошо.
Как правильно предложили выше, надо писать инт тесты. Юнит тесты - это если у вас там есть какая-то логика, которую в изоляции от внешних зависимостей можно протестировать.
ermadmi78
На мой вкус для плагинов лучше писать полноценные интеграционные тесты. Это легко делается с помощью GradleRunner:
Вот пример интеграционного теста для Gradle плагина (кодогенерация), который ищет подготовленные тесткейсы в ресурсах проекта, копирует их во временную директорию, и выполняет полноценную сборку
gradle build
с помощью GradleRunner. Затем он сравнивает результат сборки с эталонным результатом в ресурсах проекта.Есть только один минус у этого подхода - GradleRunner стартует gradle демон, что очень не быстро. Соответственно первый тесткейс у меня выполняется порядка 40 секунд, последующие по 1-2 секунды.
nikialeksey Автор
Спасибо! Это следующий этап после тестирования с помощью Composite builds :) Выглядит элегантно и позволяет делать более гибкие проверки результата тестирования. Обязательно попробую в своих проектах.