plugin development

Setup

docker pull metahelicase/gradle:2.10
git clone https://bitbucket.org/fkomauli/integration-testing
cd integration-testing
docker run --rm -v `pwd`:/project -u `id -u`:`id -g` metahelicase/gradle:2.10 check
git clone https://bitbucket.org/fkomauli/integration-testing
cd integration-testing
gradle check

Missing gradle and JDK?

about gradle

Modern build environment compatible with maven, ant, ivy, ...

Defines flexible and extensible build logic
by organizing dependent tasks in a
directed acyclic graph

Source sets separate sources by categories (main, test), not by source type

about tasks

Atomic unit of build logic

toolkit

  • build.gradle
  • Plugin<Project>
  • Task
  • META-INF/gradle-plugins/
  • org.gradle.testkit.runner.*
  • spock.lang.*

a gradle plugin is just a gradle project

written in groovy

(for fast prototyping)

or any JVM language

using the gradle API

gradle plugin

plugins {
    id 'groovy'
}

group = 'it.unimi.di'
version = '1.0-SNAPSHOT'

repositories {
    jcenter()
}

dependencies {
    compile localGroovy()
    compile gradleApi()
}
rootProject.name = 'stub'
build.gradle
settings.gradle
package stub

import org.gradle.api.Plugin
import org.gradle.api.Project

class StubPlugin implements Plugin<Project> {

    void apply(Project project) {
        project.task('stub', type: StubTask)
    }
}
package stub

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class StubTask extends DefaultTask {

    @TaskAction
    void stub() {
        logger.lifecycle("Running stub task")
    }
}

Declare the plugin class in the jar metadata

META-INF/gradle-plugins/it.unimi.di.stub.properties
implementation-class=stub.StubPlugin

parameterizing plugins

with plugin extensions

Parameters are specified as
fields or methods of a class

package stub

class StubPluginExtension {

    int repeat = 1

    String message = 'Running stub task'
}
void apply(Project project) {
    project.extensions.create('stub', StubPluginExtension)
    ...
}
@TaskAction
void stub() {
    def message = project.stub.message
    (1..project.stub.repeat).each {
        logger.lifecycle(message)
    }
}

Add the extension class to the plugin definition

and use the parameters inside the tasks

Extensions are configured inside the build script

...

apply plugin: 'it.unimi.di.stub'

stub {
    repeat 3
    message 'Custom message'
}

testing a gradle plugin

import org.gradle.testkit.runner.*
dependencies {
    ...
    testCompile 'junit:junit:4.12'
    testCompile gradleTestKit()
}

Import the gradle test kit package by adding it as test dependency to the build script

GradleRunner is a builder for test projects

It executes a build in a parallel process, then it returns a BuildResult object
to query the results

BuildResult result = GradleRunner.create()
        .withProjectDir(...)
        .withPluginClasspath(...)
        .withArguments("$task", "$arg1", "$arg2", ...)
        .build() // or .buildAndFail()

A template for plugin testing

@Rule
public final TemporaryFolder project = new TemporaryFolder()

@Before
void setup() {
    def buildScript = project.newFile 'build.gradle'
    buildScript.text = ...  // common build settings setup
}

BuildResult run(String task) {
    return GradleRunner.create()
            .withProjectDir(project.root)
            .withPluginClasspath(classpath())
            .withArguments(task, '--stacktrace')
            .build()
}

The test project's classpath should be
the runtime  classpath of the plugin itself

List<File> classpath() {
    def pluginTestClasspath = System.getProperty('plugin.test.classpath')
    return pluginTestClasspath.readLines().collect { new File(it) }
}
test {
    systemProperty 'plugin.test.classpath',
        sourceSets.main.runtimeClasspath.join('\n')
}
static final String TASK = 'integration'

@Test
void 'unit tests are run before integration tests'() {
    createPassingTestClassUnder 'test'
    createPassingTestClassUnder 'integration'
    BuildResult build = run TASK
    assertNotNull(build.task(':test'));
}

@Test
void 'integration tests are not run if unit tests fail'() {
    createFailingTestClassUnder 'test'
    createPassingTestClassUnder 'integration'
    BuildResult build = runFailing TASK
    assumeTrue(FAILED == build.task(':test').outcome);
    assertNull(build.task(":$TASK"))
}

Groovy is a language with a lighter syntax than Java and a richer library support for common operations with I/O

It is easy to design a custom DSL,
that can improve tests readability

Test builds are run in separate processes

Therefore there's no println debugging :P

Just joking, use .forwardOutput() on GradleRunner

with moderation

testing a gradle plugin
with spock

import spock.lang.*
  • Data Driven Testing (a la JUnit @Parameters)
  • Stub, Mock, Spy (a la Mockito)
  • Scenarios (a la Cucumber)
void 'testing my feature'() {
    given:
    // the environment
    when:
    // performing an action
    then:
    // assertions
}
void 'testing a functional method'() {
    expect:
    // assertions
}

Scenarios

Data Driven Testing

def 'max yields the greatest number out of two'() {
    expect:
    max(a, b) == c
    where:
    a << [5, 3, 7]
    b << [1, 9, 7]
    c << [5, 9, 7]
}

Unrolling Data Driven Testing

@Unroll
def 'max(#a, #b) yields #c'() {
    expect:
    max(a, b) == c
    where:
    a << [5, 3, 7]
    b << [1, 9, 7]
    c << [5, 9, 7]
}
Tests

max(3, 9) yields 9	0s	passed
max(5, 1) yields 5	0.004s	passed
max(7, 7) yields 7	0s	passed

Mocking

def 'subscriber should receive the message sent by the publisher'() {
    given:
    def publisher = new Publisher()
    def subscriber = Mock(Subscriber)
    publisher << subscriber
    def message = ...
    when:
    publisher.send(message)
    then:
    1 * subscriber.receive(message)
}

Gradle Plugin Development (basics)

By Francesco Komauli

Gradle Plugin Development (basics)

Introduction to gradle plugin development

  • 187