Как тестировать gradle-плагины
Когда я писал свой первый 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
-скриптов, но я выбрал более простой путь - .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.