~ read.

Сравнение способов тестирования плагинов для Gradle

Как-то при подготовке одного из докладов про Gradle и разработку плагинов для него встала задача посмотреть - а как свои поделия потестировать. Без тестов вообще жить плохо, а когда твой код реально запускается в отдельном процессе и подавно, потому что хочется дебага, хочется быстрого запуска и не хочется писать миллион example-ов, чтобы протестировать все возможные кейсы.

Подопытный кролик

Нашим подопытным кроликом будет проект, который мы с Кириллом Толкачёвым готовили для конференции JPoint 2016. Если кратко, то мы писали плагин, который будет собираться документацию из различных проектов и генерировать нормальный html с кросс-референсными ссылками. Но речь не о том, как мы писали сам плагин, а как протестировать то, что ты пишешь. Каюсь, но практически весь проект мы тестировали интеграционно через примеры. И в какой-то момент мы поняли, что стоит подумать о дргуом способе тестирования. Итак, наши кандидаты:

Задача везде одна и та же. Просто проверить, что наш плагин для документации подключен, и есть таск, который должен выполниться успешно. Погнали.

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)
    ...
}

Запускаем - работает. Это не все проблемы. Можете почитать эпичный трэд и станет совсем грустно.

Итог: направление правильно, но использование причиняет боль.

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-тестирования.

Ссылка на проект с тестами и плагином: https://github.com/aatarasoff/documentation-plugin-demo

comments powered by Disqus