使用 Gradle Kotlin DSL 进行集成测试

Posted

技术标签:

【中文标题】使用 Gradle Kotlin DSL 进行集成测试【英文标题】:Integration tests with Gradle Kotlin DSL 【发布时间】:2019-03-25 01:38:03 【问题描述】:

我正在使用this blog post 为 Spring Boot 项目配置集成测试,但我一直坚持声明源集。我也找到了this post on ***,但我想我已经走得更远了。

我的项目结构是

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

还有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)
    

我认为我的方向非常正确。至少它不再抛出异常:)

当我运行 testIntegration 任务时,我得到以下输出:

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'.

此外,IntelliJ 不会将 testIntegration 目录识别为 Kotlin 包。

【问题讨论】:

【参考方案1】:

多亏了 Kotlin Slack 频道上的一些帮助,我终于弄明白了。首先,我必须升级到 Gradle 版本 4.10.2。

有关更多信息,请查看 Gradle 的这两个页面:

https://docs.gradle.org/release-nightly/userguide/organizing_gradle_projects.html#sec:separate_test_type_source_files https://docs.gradle.org/release-nightly/userguide/organizing_gradle_projects.html#sec:separate_test_type_source_files

然后我只需要为 integrationTests 创建 sourceSets

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
    

这适用于 Java,但由于我使用的是 Kotlin,因此我不得不添加一个额外的 withConvention 包装器

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
        
    

在文档中他们只输入了runtimeClasspath += output + compileClasspath,但我添加了sourceSets["test"].runtimeClasspath,因此我可以直接使用测试依赖项,而不是为integrationTest 任务声明新的依赖项。

一旦创建了 sourceSet,就需要声明一个新任务

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

在此之后测试仍然没有运行,但那是因为我使用的是 JUnit4。所以我只需要添加 useJUnitPlatform() 这就是最终代码

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

【讨论】:

很高兴你知道了,因为它帮助我解决了我的问题。我发现很难理解何时使用“withConvention”......似乎总是在使用另一种语言如 Java 时(在我的情况下,Groovy 用于 Spock 测试)。当您想到类型系统时这是有道理的,但如果需要 withConvention(JavaXX) 而不是推断,我会发现它更一致。 这对我很有帮助,在我的情况下,我试图配置为运行 spock 和 kotlin,所以我必须做一些小改动,withConvention 的部分非常直观,最后我刚刚有了将其更改为 GroovySourceSet: withConvention(org.gradle.api.tasks.GroovySourceSet::class) groovy.srcDir("src/integration/groovy") 另外补充一点,如果你想在构建过程中运行测试,你必须在变量中设置 yoru 测试任务并将其添加到你的任务检查中: val integrationTest = task ("integrationTest") .... tasks.check dependOn(integrationTest) 【参考方案2】:

我不喜欢 withConvention 的使用以及如何设置 kotlin src 目录。所以在查看了 gradle 文档 here 和 here 之后,我想出了这个:

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)

在使用kotlin 时,我更喜欢简洁的风格,并且在新的 integrationTestTask 中使用变量。

【讨论】:

【参考方案3】:

从 Gradle 5.2.1 开始,请参阅 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) 

【讨论】:

你所做的只是从 Gradle 的文档中复制一段代码:docs.gradle.org/current/userguide/…【参考方案4】:

这里是git repo,你可以参考: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)

我希望这会有所帮助。 :)

【讨论】:

以上是关于使用 Gradle Kotlin DSL 进行集成测试的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Gradle Kotlin DSL 对 Spring Boot 应用程序进行 Dockerize

如何使用 gradle kotlin-dsl 添加新的源集

Gradle Kotlin DSL:在独特的地方定义 Kotlin 版本

使用 Gradle Kotlin DSL 发布 Kotlin MPP 元数据

gradle kotlin dsl:如何创建使用插件类的共享函数?

使用 Kotlin DSL