Niby wsio wspaniale działa, i można „żyć długo i pomyślnie”, ale jak coś nie chce zafungować, a niby ładnie skonfigurowane. Wtedy tylko cię lekki szlak trafia jak ciągle widzisz “Cannot infer Groovy class path because no Groovy Jar was found on class path”…

A więc po krótce, po co i jak. Jeżeli używamy spring inicjalizatora – https://start.spring.io/, to jak generujemy Kotlinowy projekt i jako build wybieramy Gradle to generuje nam się build.gradle.kts zamiast zwykłego build.gradle. Ma on trochę inną strukturę oraz inną składnię. Dodatkowo chciałem wydzielić osobno testy integracyjne oraz unit testy, dlatego taka zabawa z własnym buildem.

Dobrą praktyką jest mieć osobno wydzielone unit testy, a osobno testy integracyjne, dlaczego:

  • Unit test – z założenia ma być to test, który wykonuje się w izolacji od zewnętrznych systemów oraz innych komponentów aplikacji (np. baza danych, dysk, inne metody). Ma to być szybki test i powinien testować w izolacji jedną niezależną ścieżkę (logiczną, metody, biznesową). Zgodnie z piramidą testów „Unit testów” powinno być koło 70% wszystkich testów, i Unit test nie oznacza testowanie jednej metody. Dane do działania unit testa można mockować lub generować, nie mogą to być dane ładowane z zewnętrznych elementów systemu i np. w unit testach nie podnosimy context springa.
  • Testy integracyjne (integration test) – według piramidy testów powinno być ich koło 20%. Testy integracyjne testują w założeniu większe obszary aplikacji, kilka modułów, kilka logicznych ścieżek.
    W standardowej aplikacji typu Controler -> Service -> Repository test integracyjny może testować podaną ścieżkę, gdzie zapis danych nie będzie odbywał się do bazy produkcyjnej czy testowej, ale do bazy uruchomionej na czas testu, które po jego zakończeniu zostanie usunięta (np. baza typu H2 czy uruchomiona w kontenerze dockera). W testach integracyjnych możemy uruchamiać context springa czy emulować zewnętrzne serwisy http przy pomocy np. wiremocka czy używać innych narzędzi.

Dobra po omówieniu dlaczego warto rozdzielać typy testów jedna przydatna rzecz. Jeżeli chcemy w gradle wyświetlić pewne elementy to można użyć metody println, poniżej przykładowy task co na konsoli wyświetla zmienne gradle:

task print() {
    doLast {
        println 'configurations: ' + configurations
        println 'sourceSets: ' + sourceSets.main
        println 'sourceSets: ' + sourceSets.test
        println 'sourceSets: ' + sourceSets.integration
    }
}

A tutaj kompletny przykład działającego gradle.build gdzie są zdefiniowane podstawowe dependency, a osobno można uruchomić unit testy gradlew test, testy integracyjne gradlew integration, a nawet wydrukować zmienne gradle gradlew print

Cały przykładowy kod gradle.build:

plugins {
    id 'java'
    id 'groovy'
    id 'application'
    id 'org.springframework.boot' version '2.5.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'org.jetbrains.kotlin.jvm' version '1.5.21'
    id 'org.jetbrains.kotlin.plugin.spring' version '1.5.21'
}

group = "pl.net.shad.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

sourceSets {
    integration {
        java.srcDir project.file('src/integration/java')

        resources.srcDir project.file('src/integration/resources')
        resources.srcDir project.sourceSets.test.resources
        resources.srcDir project.sourceSets.main.resources

        project.plugins.withType(GroovyPlugin) {
            groovy.srcDir project.file('src/integration/groovy')
        }

        compileClasspath = sourceSets.main.output +
                configurations.compileClasspath +
                configurations.testCompileClasspath +
                configurations.testRuntimeClasspath
        runtimeClasspath = output + compileClasspath
    }
}

configurations {
    integrationCompile.extendsFrom testCompile
    integrationRuntime.extendsFrom testRuntime
}

dependencies {

    //Kotlin
    implementation "com.fasterxml.jackson.module:jackson-module-kotlin"
    implementation "org.jetbrains.kotlin:kotlin-reflect"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

    // Spring
    implementation "org.springframework.boot:spring-boot-starter-actuator"
    implementation "org.springframework.boot:spring-boot-starter-web"
    developmentOnly "org.springframework.boot:spring-boot-devtools"

    // Unit tests
    testImplementation group: 'org.codehaus.groovy', name: 'groovy', version: '3.0.8'
    testImplementation group: 'org.codehaus.groovy', name: 'groovy-all', version: '3.0.8', ext: 'pom'
    testImplementation group: 'org.spockframework', name: 'spock-core', version: '2.0-groovy-3.0'

    //Integration tests
    testImplementation group: 'org.spockframework', name: 'spock-spring', version: '2.0-groovy-3.0'
    testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '2.5.3'
    testImplementation group: 'cglib', name: 'cglib-nodep', version: '3.3.0'
}

task integration(type: Test) {
    description = 'Runs the integration tests.'
    group = 'verification'
    testClassesDirs = sourceSets.integration.output.classesDirs
    classpath = sourceSets.integration.runtimeClasspath
    outputs.upToDateWhen { false }
    mustRunAfter test
    useJUnitPlatform()
}

test {
    useJUnitPlatform()
}

task print() {
    doLast {
        println 'configurations: ' + configurations
        println 'sourceSets: ' + sourceSets.main
        println 'sourceSets: ' + sourceSets.test
        println 'sourceSets: ' + sourceSets.integration
        println 'java: ' + java
        println 'resources: ' + resources
        println 'project: ' + project
        println 'project.plugins: ' + project.plugins
        println 'project.plugins.withType(GroovyPlugin): ' + project.plugins.withType(GroovyPlugin)
        println 'dependencies: ' + dependencies
    }
}