Тестуй плагіни для Gradle правильно

Як-то при підготовці одного з доповідей про розробку плагінів для Gradle постало завдання — як свої поделия потестувати. Без тестів взагалі жити погано, а коли твій код реально запускається в окремому процесі і поготів, бо хочеться дебага, хочеться швидкого запуску і не хочеться писати мільйон example-ів, щоб протестувати всі можливі кейси. Під катом порівняння декількох способів тестування, які ми встигли спробувати.
Піддослідний кролик
Нашим піддослідним кроликом буде проект, який ми з tolkkv готували для конференції 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)
...
}

Запускаємо — працює. Це не всі проблеми. Можете почитати епічність трэд і стане зовсім сумно.
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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.