21

I'm using this blog post to configure integration tests for a Spring Boot project, but I'm pretty stuck on declaring the source sets. I also found this post on StackOverflow, but I think I'm a bit further already.

My project structure is

project
|_ src
  |_ main
  | |_ kotlin
  | |_ resources
  |_ testIntegration
  | |_ kotlin
  | |_ resources
  |_ test
  | |_ kotlin
  | |_ resources
  |_ build.gradle.kts
  |_ ... other files

And build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    idea
    kotlin("jvm")
    id("org.springframework.boot") version "2.0.5.RELEASE"
    id("org.jetbrains.kotlin.plugin.spring") version "1.2.71"
}

fun DependencyHandlerScope.springBoot(module: String) = this.compile("org.springframework.boot:spring-boot-$module:2.0.5.RELEASE")
fun DependencyHandlerScope.springBootStarter(module: String) = this.springBoot("starter-$module")

dependencies {
    springBoot("devtools")

    springBootStarter("batch")
    springBootStarter("... spring boot dependencies")


    compile("... more dependencies")

    testCompile("... more test dependencies")
}

val test by tasks.getting(Test::class) {
    useJUnitPlatform { }
}

kotlin {
    sourceSets {
        val integrationTest by creating {
            kotlin.srcDir("src/testIntegration/kotlin")
            resources.srcDir("src/testIntegration/resources")
        }
    }
}

val integrationTestCompile by configurations.creating {
    extendsFrom(configurations["testCompile"])
}
val integrationTestRuntime by configurations.creating {
    extendsFrom(configurations["testRuntime"])
}

val testIntegration by tasks.creating(Test::class) {
    group = "verification"
    testClassesDirs = kotlin.sourceSets["integrationTest"].kotlin
}

idea {
    module {
        testSourceDirs.addAll(kotlin.sourceSets["integrationTest"].kotlin.srcDirs)
        testSourceDirs.addAll(kotlin.sourceSets["integrationTest"].resources.srcDirs)
    }
}

I think I'm pretty much in the right direction. At least it doesn't throw an exception any more :)

When I run the testIntegration task, I get the following output:

Testing started at 12:08 ...
12:08:49: Executing task 'testIntegration'...

> Task :project:compileKotlin UP-TO-DATE
> Task :project:compileJava NO-SOURCE
> Task :project:processResources UP-TO-DATE
> Task :project:classes UP-TO-DATE
> Task :project:compileTestKotlin UP-TO-DATE
> Task :project:compileTestJava NO-SOURCE
> Task :project:processTestResources UP-TO-DATE
> Task :project:testClasses UP-TO-DATE
> Task :project:testIntegration
BUILD SUCCESSFUL in 2s
5 actionable tasks: 1 executed, 4 up-to-date
12:08:51: Task execution finished 'testIntegration'.

Also, IntelliJ doesn't recognise the testIntegration directories as Kotlin packages.

eskatos
  • 4,174
  • 2
  • 40
  • 42
Johan Vergeer
  • 5,208
  • 10
  • 48
  • 105

5 Answers5

26

I was finally able to figure it out thanks to some help on the Kotlin Slack channel. First of all I had to upgrade to Gradle version 4.10.2.

For more info have a look at these two pages from Gradle:

Then I just had to create the sourceSets for the integrationTests

sourceSets {
    create("integrationTest") {
            kotlin.srcDir("src/integrationTest/kotlin")
            resources.srcDir("src/integrationTest/resources")
            compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"]
            runtimeClasspath += output + compileClasspath + sourceSets["test"].runtimeClasspath
    }
}

This would work just fine for Java, but since I'm working with Kotlin I had to add an extra withConvention wrapper

sourceSets {
    create("integrationTest") {
        withConvention(KotlinSourceSet::class) {
            kotlin.srcDir("src/integrationTest/kotlin")
            resources.srcDir("src/integrationTest/resources")
            compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"]
            runtimeClasspath += output + compileClasspath + sourceSets["test"].runtimeClasspath
        }
    }
}

In the docs they only put runtimeClasspath += output + compileClasspath, but I added sourceSets["test"].runtimeClasspath so I can directly use the test dependencies instead of declaring new dependencies for the integrationTest task.

Once the sourceSets were created it was a matter of declaring a new task

task<Test>("integrationTest") {
    description = "Runs the integration tests"
    group = "verification"
    testClassesDirs = sourceSets["integrationTest"].output.classesDirs
    classpath = sourceSets["integrationTest"].runtimeClasspath
    mustRunAfter(tasks["test"])
}

After this the tests still didn't run, but that was because I'm using JUnit4. So I just had to add useJUnitPlatform() which makes this the final code

task<Test>("integrationTest") {
    description = "Runs the integration tests"
    group = "verification"
    testClassesDirs = sourceSets["integrationTest"].output.classesDirs
    classpath = sourceSets["integrationTest"].runtimeClasspath
    mustRunAfter(tasks["test"])
    useJUnitPlatform()
}
Johan Vergeer
  • 5,208
  • 10
  • 48
  • 105
  • Glad you figured it out, since it helped me with my problem. I find it difficult to understand when to use "withConvention"...it seems always when another language as Java is used (in my case Groovy for Spock tests). It makes sense when you think of the typesystem, but I would find it more consistent if the the withConvention(JavaXX) would be required instead of inferred. – lostiniceland Feb 11 '19 at 16:56
  • This really helped me, in my case I was trying to configure to run spock and kotlin, so I had to do some minor changes, the part of withConvention is pretty intuitive, at the end I just had to change it to GroovySourceSet: withConvention(org.gradle.api.tasks.GroovySourceSet::class) { groovy.srcDir("src/integration/groovy") – Damonio Aug 22 '20 at 03:16
  • Also to add that if you want to run the test as par of your build process you have to set yoru test task in a variable and add it to your task check: val integrationTest = task("integrationTest") { .... } tasks.check { dependsOn(integrationTest) } – Damonio Aug 22 '20 at 03:23
  • What's wrong with the `org.unbroken-dome.test-sets` plugin? You just use it like `testSets { create("integrationTest") }`. Creating the `sourceSets` and `task` yourself means you don't get `jacocoReport` etc. – wilmol Jul 01 '23 at 23:36
  • withConvention is deprecated in gradle-kotline-dsl-7.5. – emeraldhieu Aug 04 '23 at 10:20
6

I didnt like the use of withConvention and how the kotlin src dir was set. So after check out both gradle docs here and here, I came up with this:

sourceSets {
    create("integrationTest") {
        kotlin {
            compileClasspath += main.get().output + configurations.testRuntimeClasspath
            runtimeClasspath += output + compileClasspath
        }
    }
}

val integrationTest = task<Test>("integrationTest") {
    description = "Runs the integration tests"
    group = "verification"
    testClassesDirs = sourceSets["integrationTest"].output.classesDirs
    classpath = sourceSets["integrationTest"].runtimeClasspath
    mustRunAfter(tasks["test"])
}

tasks.check {
    dependsOn(integrationTest)
}

I preferr the less verbose style when using kotlin { and the use of variable for the new integrationTestTask.

Joker
  • 2,304
  • 25
  • 36
  • The kotlin call inside the create function call is from the project, not from the sourceset created. You can remove it and it does the same – pablisco Sep 29 '22 at 18:33
4

As of Gradle 5.2.1 see https://docs.gradle.org/current/userguide/java_testing.html#sec:configuring_java_integration_tests

sourceSets {
    create("intTest") {
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
}

val intTestImplementation by configurations.getting {
    extendsFrom(configurations.testImplementation.get())
}

configurations["intTestRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get())

dependencies {
    intTestImplementation("junit:junit:4.12")
}

val integrationTest = task<Test>("integrationTest") {
    description = "Runs integration tests."
    group = "verification"

    testClassesDirs = sourceSets["intTest"].output.classesDirs
    classpath = sourceSets["intTest"].runtimeClasspath
    shouldRunAfter("test")
}

tasks.check { dependsOn(integrationTest) }
jenglert
  • 1,589
  • 15
  • 23
  • 1
    what you only did was to copy the piece of code from Gradle's documentation: https://docs.gradle.org/current/userguide/java_testing.html#sec:configuring_java_integration_tests – jcflorezr Apr 28 '20 at 23:30
1

Here is git repo that you can refer to: enter link description here

import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent

plugins {
    application
    kotlin("jvm") version "1.3.72"
    id("com.diffplug.gradle.spotless") version "3.24.2"
    id("org.jmailen.kotlinter") version "1.26.0"
    checkstyle
}

version = "1.0.2"
group = "org.sample"

application {
    mainClass.set("org.sample.MainKt")
}

repositories {
    mavenCentral()
    jcenter()
}

tasks.checkstyleMain { group = "verification" }
tasks.checkstyleTest { group = "verification" }

spotless {
    kotlin {
        ktlint()
    }
    kotlinGradle {
        target(fileTree(projectDir).apply {
            include("*.gradle.kts")
        } + fileTree("src").apply {
            include("**/*.gradle.kts")
        })
        ktlint()
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
    testLogging {
        lifecycle {
            events = mutableSetOf(TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED)
            exceptionFormat = TestExceptionFormat.FULL
            showExceptions = true
            showCauses = true
            showStackTraces = true
            showStandardStreams = true
        }
        info.events = lifecycle.events
        info.exceptionFormat = lifecycle.exceptionFormat
    }

    val failedTests = mutableListOf<TestDescriptor>()
    val skippedTests = mutableListOf<TestDescriptor>()
    addTestListener(object : TestListener {
        override fun beforeSuite(suite: TestDescriptor) {}
        override fun beforeTest(testDescriptor: TestDescriptor) {}
        override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {
            when (result.resultType) {
                TestResult.ResultType.FAILURE -> failedTests.add(testDescriptor)
                TestResult.ResultType.SKIPPED -> skippedTests.add(testDescriptor)
                else -> Unit
            }
        }

        override fun afterSuite(suite: TestDescriptor, result: TestResult) {
            if (suite.parent == null) { // root suite
                logger.lifecycle("----")
                logger.lifecycle("Test result: ${result.resultType}")
                logger.lifecycle(
                        "Test summary: ${result.testCount} tests, " +
                                "${result.successfulTestCount} succeeded, " +
                                "${result.failedTestCount} failed, " +
                                "${result.skippedTestCount} skipped")
                failedTests.takeIf { it.isNotEmpty() }?.prefixedSummary("\tFailed Tests")
                skippedTests.takeIf { it.isNotEmpty() }?.prefixedSummary("\tSkipped Tests:")
            }
        }

        private infix fun List<TestDescriptor>.prefixedSummary(subject: String) {
            logger.lifecycle(subject)
            forEach { test -> logger.lifecycle("\t\t${test.displayName()}") }
        }

        private fun TestDescriptor.displayName() = parent?.let { "${it.name} - $name" } ?: "$name"
    })
}

dependencies {
    implementation(kotlin("stdlib"))
    implementation("com.sparkjava:spark-core:2.5.4")
    implementation("org.slf4j:slf4j-simple:1.7.30")

    testImplementation("com.squareup.okhttp:okhttp:2.5.0")
    testImplementation("io.kotest:kotest-runner-junit5-jvm:4.0.5")
    testImplementation("io.kotest:kotest-assertions-core-jvm:4.0.5") // for kotest core jvm assertions
    testImplementation("io.kotest:kotest-property-jvm:4.0.5")
}

sourceSets {
    create("integTest") {
        kotlin {
            compileClasspath += main.get().output + configurations.testRuntimeClasspath
            runtimeClasspath += output + compileClasspath
        }
    }
}

val integTest = task<Test>("integTest") {
    description = "Runs the integTest tests"
    group = "verification"
    testClassesDirs = sourceSets["integTest"].output.classesDirs
    classpath = sourceSets["integTest"].runtimeClasspath
    mustRunAfter(tasks["test"])
}

tasks.check {
    dependsOn(integTest)
}

sourceSets {
    create("journeyTest") {
        kotlin {
            compileClasspath += main.get().output + configurations.testRuntimeClasspath
            runtimeClasspath += output + compileClasspath
        }
    }
}

val journeyTest = task<Test>("journeyTest") {
    description = "Runs the JourneyTest tests"
    group = "verification"
    testClassesDirs = sourceSets["journeyTest"].output.classesDirs
    classpath = sourceSets["journeyTest"].runtimeClasspath
    mustRunAfter(tasks["integTest"])
}

tasks.check {
    dependsOn(journeyTest)
}

I hope this helps. :)

Saurav Jha
  • 31
  • 3
1

There is a dedicated Gradle feature called Declarative Test Suite that supports this case:

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter()
        }

        register("integrationTest", JvmTestSuite::class) {
            dependencies {
                implementation(project())
            }

            targets {
                all {
                    testTask.configure {
                        shouldRunAfter(test)
                    }
                }
            }
        }
    }
}

More: https://docs.gradle.org/current/userguide/java_testing.html#sec:configuring_java_integration_tests

Igor
  • 2,039
  • 23
  • 27