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