Глава 45. Gradle TestKit.

На данный момент Gradle TestKit инкубационная возможность. Пожалуйста, знайте, что его API и другие характеристики могут измениться в последних версиях Gradle.

Gradle TestKit (или просто TestKit) - библиотека, которая помогает тестировать плагины Gradle и логику сборки в целом. На данный момент, он сфокусирован на функциональном тестировании. То есть, тестирование логики сборки как часть программно выполняемой сборки. Со временем, TestKit, вероятно, будет поддерживать другие типы тестов.

45.1. Использование.

Для того, чтобы использовать TestKit, включите следующие строки в сборку вашего плагина:

Пример 45.1. Объявление зависимости TestKit

build.gradle

dependencies {
    testCompile gradleTestKit()
}
	  

gradleTestKit() включает в себя классы TestKit, а также клиента инструментального API Gradle. Он не включает версию JUnit, TestNG или любой другой фрейворк выполнения тестов. Такого рода зависимость должна быть явно объявлена.

Пример 45.2. Объявление зависимости JUnit

build.gradle

dependencies {
    testCompile 'junit:junit:4.12'
}
	  

45.2. Функциональное тестирование с Gradle Runner.

GradleRunner облегачает программное выполнение сборок Gradle и инспектирование результатов.

Неестественная сборка может быть создана (например, программно или по шаблону), которая осуществляет 'тестирование логики'. Затем сборка может быть выполнена, возможно, несколькими способами (например, различными комбинациями задачи и аргументов). Корректность логики потом может быть проконтролирована, проверив следующее, возможно, в комбинации:

  • Выходные данные сборки.
  • Лог сборки (т.е. вывод на консоль).
  • Набор задач выполненных сборкой и их результаты (например, FAILED, UP-TO-DATE и т.д.).

После создания и настройки экземпляра Gradle Runner, сборка может быть выполнена посредством методов GradleRunner.build() или GradleRunner.buildAndFail() в зависимости от ожидаемого исхода.

Следующий пример демонстрирует использование Gradle Runner в тесте Java JUnit:

Пример 45.3. Использование GradleRunner с JUnit

BuildLogicFunctionalTest.java

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import static org.gradle.testkit.runner.TaskOutcome.*;

public class BuildLogicFunctionalTest {
    @Rule public final TemporaryFolder testProjectDir = new TemporaryFolder();
    private File buildFile;

    @Before
    public void setup() throws IOException {
        buildFile = testProjectDir.newFile("build.gradle");
    }

    @Test
    public void testHelloWorldTask() throws IOException {
        String buildFileContent = "task helloWorld {" +
                                  "    doLast {" +
                                  "        println 'Hello world!'" +
                                  "    }" +
                                  "}";
        writeFile(buildFile, buildFileContent);

        BuildResult result = GradleRunner.create()
            .withProjectDir(testProjectDir.getRoot())
            .withArguments("helloWorld")
            .build();

        assertTrue(result.getOutput().contains("Hello world!"));
        assertEquals(result.task(":helloWorld").getOutcome(), SUCCESS);
    }

    private void writeFile(File destination, String content) throws IOException {
        BufferedWriter output = null;
        try {
            output = new BufferedWriter(new FileWriter(destination));
            output.write(content);
        } finally {
            if (output != null) {
                output.close();
            }
        }
    }
}
	  

Использоваться может любой фреймворк исполнения тестов.

Так как сборочные скрипты Gradle написаны на языке программирования Groovy и множество плагинов тоже реализовано на нем, то чаще всего писать на нем функциональные тесты продуктивный выбор. Более того, рекомендуется использовать (основанный на Groovy) фреймворк выполнения тестов Spock, так как он предлагает много мощных возможностей по сравнению с JUnit.

Следующий пример демонстрирует использование Gradle Runner в тесте Groovy Spock:

Пример 45.4. Использование GradleRunner со Spock

BuildLogicFunctionalTest.groovy

import org.gradle.testkit.runner.GradleRunner
import static org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification

class BuildLogicFunctionalTest extends Specification {
    @Rule final TemporaryFolder testProjectDir = new TemporaryFolder()
    File buildFile

    def setup() {
        buildFile = testProjectDir.newFile('build.gradle')
    }

    def "hello world task prints hello world"() {
        given:
        buildFile << """
            task helloWorld {
                doLast {
                    println 'Hello world!'
                }
            }
        """

        when:
        def result = GradleRunner.create()
            .withProjectDir(testProjectDir.root)
            .withArguments('helloWorld')
            .build()

        then:
        result.output.contains('Hello world!')
        result.task(":helloWorld").outcome == SUCCESS
    }
}
	  

Обычная практика - реализовывать любую пользовательскую логику (такую как плагины и типы задач), которая более сложная по своей природе чем внешние классы, в автономном проекте. Основаная идея этого подхода заключается в упаковке скомпилированного кода в jar-файл, публикации его в двоичное хранилище и использование в нескольких проектах.

45.3. Использование тестируемого плагина в тестовой сборке.

GradleRunner использует инструментальное API для выполнения сборок. Из этого следует вывод, что сборки выполняются в отдельном процессе (т.е. это не тот же процесс, что выполняет тесты). Таким образом, у тестовой сборки не тот же путь к классам и загрузчики классов, что и у тестового процесса и по умолчанию тестируемый код недоступен тестовой сборке.

Начиная с версии 2.13, Gradle предоставляет стандратный механизм внедрения тестового кода в тестовую сборку.

Для более ранних версий Gradle (до 2.13) возможно вручную, посредством дополнительной конфигурации, сделать тестируемый код доступным. Следующий пример демонстрирует сборку, генерирующую файл, содержащий путь к классам реализации тестируемого кода и делающую его доступным в тесте.

Пример 45.5. Делаем тестируемый код доступным тестам

build.gradle

// Write the plugin's classpath to a file to share with the tests
task createClasspathManifest {
    def outputDir = file("$buildDir/$name")

    inputs.files sourceSets.main.runtimeClasspath
    outputs.dir outputDir

    doLast {
        outputDir.mkdirs()
        file("$outputDir/plugin-classpath.txt").text = sourceSets.main.runtimeClasspath.join("\n")
    }
}

// Add the classpath file to the test runtime classpath
dependencies {
    testRuntime files(createClasspathManifest)
}
	  

Примечание: код этого примера можно найти в папке samples/testKit/gradleRunner/manualClasspathInjection в дистрибутиве Gradle '-all'.

Затем тесты могут прочитать это значение и внедрить путь к классам в тестовую сборку, используя метод GradleRunner.withPluginClasspath(java.lang.Iterable). Потом этот путь к классам можно использовать для того, чтобы найти плагины в тестовой сборке посредством DSL (смотрите Главу 27 Плагины Gradle). Применение плагинов с помощью DSL требует задание идентификатора плагина. Ниже есть пример (на Groovy) как это сделать в рамках метода setup() фреймворка Spock, который аналогичен методу @Before из JUnit.

Этот подход хорошо работает, когда функциональные тесты выполняются как часть сборки Gradle. Когда они выполняеются из IDE, есть дополнительные соображения. А именно, файл манифеста пути к классам, указывающий на файлы классов, должен быть сгенерирован Gradle, а не IDE. Это означает, что после внесения изменения в исходный тестируемый код, он должен быть повторно скомпилирован Gradle. Точно также, если действующий путь к классам тестируемого кода изменился, манифест должен быть сгенерирован повторно. В любом случае, выполнение задачи testClasses гарантирует, что все актуально.

45.3.1. Работа с версиями Gradle до версии 2.8.

Метод GradleRunner.withPluginClasspath(java.lang.Iterable) не сработает, если будет выполнен версией Gradle младше 2.8 (смотрите Секцию 45.5 Версии Gradle используемые для тестирования), так как эта функция не поддерживается такими версиями.

Вместо этого, код должен быть внедрен посредством самого сборочного скрипта. Следующий пример показывает как это должно быть сделано.

Пример 45.6. Внедрение тестируемого кода классов в тестовые сборки

src/test/groovy/org/gradle/sample/BuildLogicFunctionalTest.groovy

List<File> pluginClasspath

def setup() {
    buildFile = testProjectDir.newFile('build.gradle')

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

def "hello world task prints hello world"() {
    given:
    buildFile << """
        plugins {
            id 'org.gradle.sample.helloworld'
        }
    """

    when:
    def result = GradleRunner.create()
        .withProjectDir(testProjectDir.root)
        .withArguments('helloWorld')
        .withPluginClasspath(pluginClasspath)
        .build()

    then:
    result.output.contains('Hello world!')
    result.task(":helloWorld").outcome == SUCCESS
}
	  

Примечание: код этого примера можно найти в папке samples/testKit/gradleRunner/manualClasspathInjection дистрибутива Gradle '-all'.

src/test/groovy/org/gradle/sample/BuildLogicFunctionalTest.groovy

List<File> pluginClasspath

def setup() {
    buildFile = testProjectDir.newFile('build.gradle')

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

def "hello world task prints hello world with pre Gradle 2.8"() {
    given:
    def classpathString = pluginClasspath
        .collect { it.absolutePath.replace('\\', '\\\\') } // escape backslashes in Windows paths
        .collect { "'$it'" }
        .join(", ")

    buildFile << """
        buildscript {
            dependencies {
                classpath files($classpathString)
            }
        }
        apply plugin: "org.gradle.sample.helloworld"
    """

    when:
    def result = GradleRunner.create()
        .withProjectDir(testProjectDir.root)
        .withArguments('helloWorld')
        .withGradleVersion("2.7")
        .build()

    then:
    result.output.contains('Hello world!')
    result.task(":helloWorld").outcome == SUCCESS
}
	  

Примечание: код этого примера можно найти в папке samples/testKit/gradleRunner/manualClasspathInjection дистрибутива Gradle '-all'.

45.3.2. Автоматическое внедрение с помощью плагина разработки Java Gradle.

Плагин разработки Java Gradle может помочь в разработке плагинов Gradle. Начиная с версии Gradle 2.13, он предоставляет прямую интеграцию с TestKit. Когда он применяется к проекту, плагин автоматически добавляет зависимость gradleTestKit() к конфигурации test compile. Более того, он автоматически генерирует путь к классам тестируемого кода и внедряет его посредством GradleRunner.withPluginClasspath() для каждого экземпляра GradleRunner, созданного пользователем. Если целевая версия Gradle младше 2.8, автоматическое внедрение пути к классам плагина не производится.

Плагин использует следующие соглашения для применения зависимости TestKit и внедрения пути к классам:

  • Набор исходных кодов, содержащим тестируемый код: sourceSets.main
  • Набор исходные кодов для внедрения пути к классам плагина: sourceSets.test

Любое из этих соглашений может быть перенастроено с помощью класса GradlePluginDevelopmentExtension.

Следующий пример, написанный на Groovy, демонстрирует как автоматически внедрить путь к классам плагина с использованием стандратных соглашений, примененных плагином разработки Java Gradle.

Пример 45.7. Использование плагина разработки Java Gradle для генерации метаданных плагина

build.gradle

apply plugin: 'groovy'
apply plugin: 'java-gradle-plugin'

dependencies {
    testCompile('org.spockframework:spock-core:1.0-groovy-2.4') {
        exclude module: 'groovy-all'
    }
}
	  

Примечание: код этого примера можно найти в папке samples/testKit/gradleRunner/automaticClasspathInjectionQuickstart дистрибутива Gradle '-all'.

Пример 45.8. Автоматическое внедрение классов тестируемого кода в тестовые сборки

src/test/groovy/org/gradle/sample/BuildLogicFunctionalTest.groovy

def "hello world task prints hello world"() {
    given:
    buildFile << """
        plugins {
            id 'org.gradle.sample.helloworld'
        }
    """

    when:
    def result = GradleRunner.create()
        .withProjectDir(testProjectDir.root)
        .withArguments('helloWorld')
        .withPluginClasspath()
        .build()

    then:
    result.output.contains('Hello world!')
    result.task(":helloWorld").outcome == SUCCESS
}
	  

Примечание: код этого примера можно найти в папке samples/testKit/gradleRunner/automaticClasspathInjectionQuickstart дистрибутива Gradle '-all'.

Следующий сборочный скрипт демонстрирует как перенастроить соглашения, предоставленные плагином разработки Java Gradle, для проекта, который использует пользовательский набор исходных кодов Test.

Пример 45.9. Перенастройка соглашений о генерации пути к классам плагина разработки Java Gradle

build.gradle

apply plugin: 'groovy'
apply plugin: 'java-gradle-plugin'

sourceSets {
    functionalTest {
        groovy {
            srcDir file('src/functionalTest/groovy')
        }
        resources {
            srcDir file('src/functionalTest/resources')
        }
        compileClasspath += sourceSets.main.output + configurations.testRuntime
        runtimeClasspath += output + compileClasspath
    }
}

task functionalTest(type: Test) {
    testClassesDir = sourceSets.functionalTest.output.classesDir
    classpath = sourceSets.functionalTest.runtimeClasspath
}

check.dependsOn functionalTest

gradlePlugin {
    testSourceSets sourceSets.functionalTest
}

dependencies {
    functionalTestCompile('org.spockframework:spock-core:1.0-groovy-2.4') {
        exclude module: 'groovy-all'
    }
}
	  

Примечание: код этого примера можно найти в папке samples/testKit/gradleRunner/automaticClasspathInjectionCustomTestSourceSet дистрибутива Gradle '-all'.

45.4. Контроль окружения сборки.

Запускатель выполняет тестовые сборки в изолированном окружении, указывая предназначенную для этого 'рабочую папку' в папке внутри временной папки JVM (т.е. той, которая задана системным свойством java.io.tmpdir, обычно /tmp). Никакая конфигурация из пользовательской домашней папки Gradle, заданной по умолчанию, (например, ~/.gradle/gradle.properties) не используется в тестовом выполнении. TestKit не предоставляет механизма для тонкого контроля переменных окружения и так далее. Будущие версии TestKit будут предоставлять улучшенные опции настройки.

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

45.5. Версия Gradle, используемая для теста.

Запускателю Gradle требуется дистрибутив для того, чтобы выполнить сборку. TestKit полагается не на все реализации Gradle.

По умолчанию, запускатель пытается найти дистрибутив Gradle, опираясь на то, откуда был загружен класс GradleRunner. То есть, он ожидает, что класс был загружен из дистрибутива Gradle, как в случае использования объявления зависимости gradleTestKit().

Когда запускатель используется как часть тестов, выполняемых Gradle (например, при выполнении задачи test плагина проекта), тот же дистрибутив, который используется для выполнения тестов, будет использован для запускателя. Когда запускатель используется как часть тестов, выполняемых IDE, тот же дистрибутив Gradle, который был использован при импорте проекта, будет использован и для него. Это означается, что, в сущности, плагин будет тестироваться с той же самой версией Gradle, которой он был собран.

В качестве альтернативы, другая конкретная версия Gradle может быть использована любым из следующих методов:

Потенциально, это можно использовать для тестирования логики сборки в нескольких версиях Gradle. Следующий пример демонстрирует межверсионно совместимый тест, написанный как Groovy Spock тест:

Пример 45.10. Указание версии Gradle для тестового выполнения

BuildLogicFunctionalTest.groovy

import org.gradle.testkit.runner.GradleRunner
import static org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification
import spock.lang.Unroll

class BuildLogicFunctionalTest extends Specification {
    @Rule final TemporaryFolder testProjectDir = new TemporaryFolder()
    File buildFile

    def setup() {
        buildFile = testProjectDir.newFile('build.gradle')
    }

    @Unroll
    def "can execute hello world task with Gradle version #gradleVersion"() {
        given:
        buildFile << """
            task helloWorld {
                doLast {
                    logger.quiet 'Hello world!'
                }
            }
        """

        when:
        def result = GradleRunner.create()
            .withGradleVersion(gradleVersion)
            .withProjectDir(testProjectDir.root)
            .withArguments('helloWorld')
            .build()

        then:
        result.output.contains('Hello world!')
        result.task(":helloWorld").outcome == SUCCESS

        where:
        gradleVersion << ['2.6', '2.7']
    }
}
	  

45.5.1. Поддержка функций при тестировании с различными версиями Gradle.

Есть возможность использовать GradleRunner для выполнения сборок с Gradle 1.0 и выше. Однаком, некоторые функции запускателя не поддерживаются в более ранних версиях. В таких случаях, запускатель выбросит исключение при попытке использовать функцию.

Следующая таблица перечисляет функции, которые чуствительны к используемой версии Gradle.

Таблица 45.1. Совместимость версий Gradle
ФункцияМинимальная версияОписание
Изучение выполненных задач2.5Изучение выполненных задач с использованием BuildResult.getTasks() и похожих методов.
Внедрение пути к классам плагина2.8Внедрение тестируемого кода посредством GradleRunner.withPluginClasspath(java.lang.Iterable).
Изучение результата сборки в отладочном режиме2.9Изучение текстового вывода сборки при запуске в отладочном режиме с использованием BuildResult.getOutput().
Автоматическое внедрение пути к классам плагина2.13Внедрение тестируемого кода автоматически посредством GradleRunner.withPluginClasspath(), применим плагин разработки Java Gradle.

45.6. Отладка логики сборки.

Запускатель использует инструментальное API для выполнения сборок. Из этого следует, что сборки выполняются в отдельном процессе (то есть не тот же процесс, что выполняет тесты). Таким образом, выполнение ваших тестов в отладочном режиме не позволяет вам отлаживать логику сборки, как вы могли бы ожидать. Ни одна точка останова установленная в вашей IDE не будет активирована кодом тестируемым сборкой.

TestKit предоставляет два различных способа включения отладочного режима:

  • Установка системного свойства 'org.gradle.testkit.debug' в true для JVM используя GradleRunner (то есть не сборка выполненная запускателем).
  • Вызов метода GradleRunner.withDebug(boolean).

Подход с системным свойством может использоваться, когда желательно включение поддержки отладки без специального изменения конфигурации запускателя. Большинство IDE предлагает возможность установки системных свойств JVM для тестового выполнения и такая возможность может быть использована для установки этого системного свойства.

45.7. Тестирование с кэшем сборки.

Для включения кэша сборки в ваших тестах, вы можете передать аргумент --build-cache объекту GradleRunner или использовать один из методов, описанных в Секции 15.2 Включение кэша сборки. Потом вы можете проверить результат задачи TaskOutcome.FROM_CACHE, когда ваша задача будет закэширована. Этот результат действителен для Gradle 3.5 и новее.

Пример 45.11. Тестирование кэшируемых задач

BuildLogicFunctionalTest.groovy

def "cacheableTask is loaded from cache"() {
    given:
    buildFile << """
        plugins {
            id 'org.gradle.sample.helloworld'
        }
    """

    when:
    def result = runner()
        .withArguments( '--build-cache', 'cacheableTask')
        .build()

    then:
    result.task(":cacheableTask").outcome == SUCCESS

    when:
    new File(testProjectDir.root, 'build').deleteDir()
    result = runner()
        .withArguments( '--build-cache', 'cacheableTask')
        .build()

    then:
    result.task(":cacheableTask").outcome == FROM_CACHE
}