Как-то при подготовке одного из докладов про разработку плагинов для Gradle встала задача — как свои поделия потестировать. Без тестов вообще жить плохо, а когда твой код реально запускается в отдельном процессе и подавно, потому что хочется дебага, хочется быстрого запуска и не хочется писать миллион example-ов, чтобы протестировать все возможные кейсы. Под катом сравнение нескольких способов тестирования, которые мы успели попробовать.
Подопытный кролик
Нашим подопытным кроликом будет проект, который мы с tolkkv готовили для конференции JPoint 2016. Если кратко, то мы писали плагин, который будет собирать документацию из различных проектов и генерировать обычный html-документ с кросс-референсными ссылками. Но речь не о том, как мы писали сам плагин (хотя это тоже было весело и увлекательно), а как протестировать то, что ты пишешь. Каюсь, но практически весь проект мы тестировали интеграционно, через примеры. И в какой-то момент поняли, что стоит подумать о другом способе тестирования. Итак, наши кандидаты:
- Gradle Test Kit (https://docs.gradle.org/current/userguide/test_kit.html)
- Nebula Test (https://github.com/nebula-plugins/nebula-test)
- Unit Test как они есть (пример тут: https://github.com/spring-gradle-plugins/dependency-management-plugin)
Задача везде одна и та же. Просто проверить, что наш плагин для документации подключен, и есть таск, который способен выполниться успешно. Погнали.
Gradle Test Kit
Сейчас находится в стадии инкубации, что было сильно заметно, когда мы пытались его прикрутить. Если взять пример из документации и наивно его применить к нашим реалиям (см. пример ниже), то ничего не заработает. Давайте разбираться, а что мы сделали.
@Slf4j class TestSpecification extends Specification { @Rule final TemporaryFolder testProjectDir = new TemporaryFolder() def buildFile def setup() { buildFile = testProjectDir.newFile('build.gradle') } def "execution of documentation distribution task is up to date"() { given: buildFile << """ buildscript { repositories { jcenter() } dependencies { classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3' } } apply plugin: 'org.asciidoctor.convert' apply plugin: 'ru.jpoint.documentation' docs { debug = true } dependencies { asciidoctor 'org.asciidoctor:asciidoctorj:1.5.4' docs 'org.slf4j:slf4j-api:1.7.2' } """ when: def result = GradleRunner.create() .withProjectDir(testProjectDir.root) .withArguments('documentationDistZip') .build() then: result.task(":documentationDistZip").outcome == TaskOutcome.UP_TO_DATE } }
Мы используем Spock, хотя можно использовать и JUnit. Наш проект будет лежать и запускаться во временной папке, которая определяется через testProjectDir
. В методе setup мы создаём новый файл сборки проекта. В given мы определили контент этого файла, подключили к нему необходимые нам плагины и зависимости. В секции when через новый класс GradleRunner
, мы передаём определённую ранее директорию с проектом и говорим, что хотим запустить таск из плагина. В секции then мы проверяем, что таск у нас есть, но так как никаких документов мы не определили, то исполнять его не нужно.
Дак вот, запустив тест, мы узнаем, что тестовый фреймворк не знает что за плагин — ru.jpoint.documentation
— мы подключили. Почему так происходит? Потому что сейчас GradleRunner
не передаёт внутрь себя classpath плагина. А это очень сильно ограничивает нас в тестировании. Идём в документацию и узнаём, что есть метод withPluginClasspath
, в который можно передать нужные нам ресурсы, и они подхвачены в процессе тестирования. Осталось понять — как его сформировать.
Если думаете, что это очевидно, подумайте ещё раз. Чтобы решить проблему, нужно самому через отдельный таск (спасибо Gradle за императивный подход) сформировать текстовый файл с набором ресурсов в build
директории. Пишем:
task createClasspathManifest { def outputDir = sourceSets.test.output.resourcesDir inputs.files sourceSets.main.runtimeClasspath outputs.dir outputDir doLast { outputDir.mkdirs() file("$outputDir/plugin-classpath.txt").text = sourceSets.main.runtimeClasspath.join("\n") } }
Запускаем, получаем файлик. Теперь идём в наш тест и в setup добавляем следующий приятный для чтения код:
def pluginClasspathResource = getClass().classLoader.findResource("plugin-classpath.txt") if (pluginClasspathResource == null) { throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.") } pluginClasspath = pluginClasspathResource.readLines() .collect { new File(it) }
Теперь передадим classpath
в GradleRunner
. Запустим, и ничего не работает. Идём на форумы и узнаём, что это работает только с Gradle 2.8+. Проверяем, что у нас 2.12 и грустим. Что делать? Попробуем сделать как советуют делать для Gradle 2.7 и ниже. Мы сами сформируем ещё один classpath
и добавим его напрямую в buildscript
:
def classpathString = pluginClasspath .collect { it.absolutePath.replace('\\', '\\\\') } .collect { "'$it'" } .join(", ")
dependencies { classpath files($classpathString) ... }
Запускаем — работает. Это не все проблемы. Можете почитать эпичный трэд и станет совсем грустно.
2.13 update: когда мы экспериментировали, новая версия ещё не вышла. В ней исправили (наконец-то) проблему с подтягиванием ресурсов и теперь код выглядит куда пристойнее благороднее. Для этого нужно немного по-другому подключить плагин:
plugins { id 'ru.jpoint.documentation' }
и запускать GradleRunner
с пустым classpath-ом:
def result = GradleRunner.create() .withProjectDir(testProjectDir.root) .withArguments('documentationDistZip') .withPluginClasspath() .build()
Осталось лишь огорчение, что из Idea нельзя запустить этот тест через контекстное меню, потому что она не умеет правильно подставлять нужные ресурсы. Через ./gradlew
всё прекрасно работает.
Итог: направление правильно, но использование порой причиняет боль.
Nebula Test
Второй кандидат показал себя куда лучше. Всё что нужно сделать — подключить плагин в свои зависимости:
functionalTestCompile 'com.netflix.nebula:nebula-test:4.0.0'
Затем в спецификации мы можем по аналогии с прошлым примером создать build.gradle
файл:
def setup() { buildFile << """ buildscript { repositories { jcenter() } dependencies { classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3' } } apply plugin: 'org.asciidoctor.convert' apply plugin: info.developerblog.documentation.plugin.DocumentationPlugin docs { debug = true } dependencies { asciidoctor 'org.asciidoctor:asciidoctorj:1.5.4' docs 'org.slf4j:slf4j-api:1.7.2' } """ }
А вот сам тест выглядит легко, понятно, а самое главное — он запускается без приседаний:
def "execution of documentation distribution task is success"() { when: createFile("/src/docs/asciidoc/documentation.adoc") ExecutionResult executionResult = runTasksSuccessfully('documentationDistZip') then: executionResult.wasExecuted('documentationDistZip') executionResult.getSuccess() }
В этом примере мы ещё и создали файл с документацией, и поэтому результат исполнения нашего таска будет SUCCESS
.
Итог: всё очень здорово. Рекомендуется к использованию.
Unit тестирование
Ок, всё что мы делали ранее это всё-такие интеграционные тесты. Посмотрим, что мы можем сделать через механизм Unit-тестов.
Сначала сконфигурируем проект просто через код:
def setup() { project = new ProjectBuilder().build() project.buildscript.repositories { jcenter() } project.buildscript.dependencies { classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3' } project.plugins.apply('org.asciidoctor.convert') project.plugins.apply(DocumentationPlugin.class) project.dependencies { asciidoctor 'org.asciidoctor:asciidoctorj:1.5.4' docs 'org.slf4j:slf4j-api:1.7.2' } }
Как видно, это практически ничем не отличается от того, что мы писали раньше, только Closure
пишутся несколько длиннее.
Теперь мы можем протестировать, что наш таск из плагина действительно появился в сконфигурированном проекте (и вообще конфигурирование прошло успешно):
def "execution of documentation distribution task is success"() { when: project then: project.getTasksByName('documentationDistZip', true).size() == 1 }
Но больше этого мы протестировать не можем. То есть через этот способ нам не понять, что таск будет делать то, что ему положено, и, скажем, документ действительно будет сформирован.
Итог: можно использовать для проверки конфигурации проектов. Это быстрее, чем тестирование через реальное выполнение. Но возможности у нас сильно ограничены.
Резюме
Рекомендуется использование Nebula Test
для тестирования плагинов. Если у вас есть развесистая логика при конфигурации проекта, то имеет смысл посмотреть в сторону Unit-тестирования. Ну и ждём допиленный Gradle Test Kit
.
Ссылка на проект с тестами и плагином: https://github.com/aatarasoff/documentation-plugin-demo
ссылка на оригинал статьи https://habrahabr.ru/post/282924/
Добавить комментарий