Spring Cloud Contract契约测试在Gradle Kotlin DSL中Stub下载目录配置?ContractDsl与StubDownloader

Spring Cloud Contract:Gradle Kotlin DSL 中的 Stub 下载目录配置深度剖析

大家好!今天我们来深入探讨 Spring Cloud Contract (SCC) 在 Gradle Kotlin DSL 环境下的契约测试,特别是关于 Stub 下载目录的配置问题。我们将从 Contract DSL 的基本概念出发,逐步深入到 StubDownloader 的配置细节,并通过实际代码示例来展示如何有效地管理 Stub 文件。

1. Spring Cloud Contract 简介

Spring Cloud Contract 提供了一种契约驱动开发(Contract-Driven Development,CDD)的解决方案。它允许消费者和生产者之间通过契约来定义服务之间的交互。这些契约可以用来生成 Stub (Mock),用于消费者端的单元测试,以及用于生产者端的集成测试,从而确保服务之间的兼容性。

关键概念:

  • 契约 (Contract): 定义了消费者期望从生产者那里获得什么。通常使用 Groovy DSL 或 YAML 编写。
  • Stub: 基于契约生成的 Mock 服务。消费者可以使用 Stub 来模拟生产者,进行单元测试。
  • 生产者端测试: 基于契约生成的集成测试,验证生产者是否满足契约。
  • 消费者端测试: 使用 Stub 模拟生产者,验证消费者是否按照契约消费服务。

2. Gradle Kotlin DSL 集成 Spring Cloud Contract

在 Gradle Kotlin DSL 中集成 Spring Cloud Contract,我们需要添加相应的依赖:

plugins {
    id("org.springframework.cloud.contract") version "4.0.0" // 使用最新版本
}

dependencies {
    testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
    testImplementation("org.springframework.cloud:spring-cloud-contract-stub-runner") // 如果需要在测试时下载 Stubs
    testImplementation("org.springframework.boot:spring-boot-starter-test") // 包含 JUnit 等测试框架
}

contracts {
    testFramework = "Junit5" // 使用 JUnit 5
    testMode = "WEB_TEST_CLIENT" // 使用 WebTestClient 进行测试
    // 其他配置...
}

这段代码声明了 Spring Cloud Contract Gradle 插件,并添加了必要的依赖。spring-cloud-starter-contract-verifier 用于生产者端测试,spring-cloud-contract-stub-runner 用于消费者端下载 Stubs。contracts 块用于配置契约相关的参数。

3. Contract DSL 与 Stub 下载

Spring Cloud Contract 提供了 contracts 闭包,允许我们配置契约相关的行为。其中,与 Stub 下载相关的配置主要集中在 stubRunner 部分。

基本 Stub 下载配置:

最简单的 Stub 下载方式是使用 stubRunner.downloadRemote,它会从 Maven 仓库下载 Stub。

contracts {
    stubRunner {
        downloadRemote {
            groupId = "com.example"
            artifactId = "producer"
            version = "1.0.0"
            repositoryRoot = "http://nexus.example.com/repository/maven-public/"
        }
    }
}

这段代码指示 stubRunnerhttp://nexus.example.com/repository/maven-public/ 仓库下载 com.example:producer:1.0.0 的 Stub。

问题:Stub 下载目录的配置

默认情况下,下载的 Stub 文件会被保存在一个默认的临时目录中。但是在某些情况下,我们需要自定义 Stub 下载目录,例如:

  • 更好的可维护性: 方便查找和管理 Stub 文件。
  • 共享 Stub 文件: 允许多个模块或项目共享同一个 Stub 文件。
  • 定制化 Stub 文件处理: 在 Stub 文件下载后,进行额外的处理(例如,解压缩、修改等)。

4. 使用 StubDownloader 配置 Stub 下载目录

StubDownloader 是 Spring Cloud Contract 中负责 Stub 下载的组件。我们可以通过配置 StubDownloader 来定制 Stub 下载的行为,包括指定下载目录。

配置方式:

虽然 contracts 闭包提供了 stubRunner 配置,但直接配置 StubDownloader 的下载目录并不直接支持。我们需要通过自定义任务或配置 StubRunnerOptions 来间接实现。

方法一:自定义 Gradle 任务

我们可以创建一个自定义 Gradle 任务,该任务负责下载 Stub 文件到指定目录。

tasks.register<Task>("downloadStubs") {
    doLast {
        val stubRunnerOptions = StubRunnerOptionsBuilder.newBuilder()
            .withStubsMode(StubRunnerProperties.StubsMode.REMOTE)
            .withStubsRepositoryRoot("http://nexus.example.com/repository/maven-public/")
            .withMinPort(8081)
            .withMaxPort(8090)
            .withDownloadStub("com.example:producer:1.0.0")
            .build()
        val stubDownloader = StubDownloader(stubRunnerOptions)
        val destinationDir = File(project.buildDir, "stubs") // 自定义下载目录
        if (!destinationDir.exists()) {
            destinationDir.mkdirs()
        }

        val downloadedStubs = stubDownloader.downloadAndUnpackStubs(stubRunnerOptions.downloadedStubs)
        downloadedStubs.forEach { stub ->
            val targetFile = File(destinationDir, stub.file.name)
            stub.file.copyTo(targetFile, overwrite = true)
            println("Downloaded stub to: ${targetFile.absolutePath}")
        }

    }
}

tasks.named("test") {
    dependsOn("downloadStubs") // 确保测试前先下载 Stubs
}

代码解释:

  1. tasks.register<Task>("downloadStubs"): 注册一个名为 downloadStubs 的 Gradle 任务。
  2. StubRunnerOptionsBuilder: 构建 StubRunnerOptions 对象,配置 Stub 下载的参数,例如:
    • stubsMode(StubRunnerProperties.StubsMode.REMOTE): 指定从远程仓库下载 Stubs。
    • stubsRepositoryRoot(...): 指定 Maven 仓库的地址。
    • downloadStub(...): 指定需要下载的 Stub 的 GroupId、ArtifactId 和 Version。
  3. StubDownloader(stubRunnerOptions): 创建 StubDownloader 实例,并传入配置好的 StubRunnerOptions
  4. destinationDir: 定义 Stub 文件的下载目录。这里使用 project.buildDir 作为基础目录,并在其下创建 stubs 目录。
  5. downloadAndUnpackStubs(...): 调用 StubDownloader 的方法下载并解压缩 Stubs。
  6. stub.file.copyTo(...): 将下载的 Stub 文件复制到指定目录。
  7. tasks.named("test") { dependsOn("downloadStubs") }: 配置 test 任务依赖于 downloadStubs 任务,确保在运行测试之前先下载 Stubs。

方法二:配置 StubRunnerOptions 并自定义 StubDownloader

这种方法更加灵活,可以自定义 StubDownloader 的行为,并在其中处理下载目录。

首先,创建一个自定义的 StubDownloader

class CustomStubDownloader(stubRunnerOptions: StubRunnerOptions) : StubDownloader(stubRunnerOptions) {

    private val destinationDir: File = File(System.getProperty("java.io.tmpdir"), "custom-stubs")

    init {
        if (!destinationDir.exists()) {
            destinationDir.mkdirs()
        }
    }

    override fun downloadAndUnpackStubs(stubsToDownload: MutableList<DownloadedStub>): MutableList<DownloadedStub> {
        val downloadedStubs = super.downloadAndUnpackStubs(stubsToDownload)
        return downloadedStubs.map { downloadedStub ->
            val targetFile = File(destinationDir, downloadedStub.file.name)
            downloadedStub.file.copyTo(targetFile, overwrite = true)
            downloadedStub.apply {
                file = targetFile
            }
        }.toMutableList()
    }
}

然后,在测试类中使用自定义的 StubDownloader

@SpringBootTest
@AutoConfigureStubRunner(
    stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    ids = ["com.example:producer:+:stubs"],
    repositoryRoot = "http://nexus.example.com/repository/maven-public/",
    downloadedStubs = "custom-stubs"
)
class ConsumerApplicationTests {

    @Autowired
    lateinit var restTemplate: RestTemplate

    @Test
    fun contextLoads() {
        val response = restTemplate.getForObject("/message", String::class.java)
        println(response)
        assertThat(response).isEqualTo("Hello from Producer")
    }
}

代码解释:

  1. CustomStubDownloader: 继承自 StubDownloader,重写 downloadAndUnpackStubs 方法。
  2. destinationDir: 定义 Stub 文件的下载目录。这里使用 System.getProperty("java.io.tmpdir") 作为基础目录,并在其下创建 custom-stubs 目录。
  3. super.downloadAndUnpackStubs(...): 调用父类的 downloadAndUnpackStubs 方法,执行原始的 Stub 下载逻辑。
  4. stub.file.copyTo(...): 将下载的 Stub 文件复制到指定目录。
  5. downloadedStub.apply { file = targetFile }: 更新 DownloadedStub 对象中的 file 属性,指向新的 Stub 文件位置。
  6. @AutoConfigureStubRunner(...): 使用 downloadedStubs = "custom-stubs" 指定自定义目录的名称。需要注意的是,这里传递的只是一个目录名称,而不是完整的路径。Spring Cloud Contract 会将该名称与临时目录组合,形成最终的下载目录。

方法三:使用 StubRunnerConfiguration 定制

这种方法允许更细粒度的控制,可以自定义整个 StubRunner 的配置过程。

创建一个自定义的 StubRunnerConfiguration 类:

@Configuration
class CustomStubRunnerConfiguration {

    @Bean
    fun customStubDownloader(stubRunnerOptions: StubRunnerOptions): StubDownloader {
        val destinationDir = File(System.getProperty("java.io.tmpdir"), "custom-stubs")
        if (!destinationDir.exists()) {
            destinationDir.mkdirs()
        }

        return object : StubDownloader(stubRunnerOptions) {
            override fun downloadAndUnpackStubs(stubsToDownload: MutableList<DownloadedStub>): MutableList<DownloadedStub> {
                val downloadedStubs = super.downloadAndUnpackStubs(stubsToDownload)
                return downloadedStubs.map { downloadedStub ->
                    val targetFile = File(destinationDir, downloadedStub.file.name)
                    downloadedStub.file.copyTo(targetFile, overwrite = true)
                    downloadedStub.apply {
                        file = targetFile
                    }
                }.toMutableList()
            }
        }
    }
}

在测试类中,引入自定义的 StubRunnerConfiguration

@SpringBootTest
@AutoConfigureStubRunner(
    stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    ids = ["com.example:producer:+:stubs"],
    repositoryRoot = "http://nexus.example.com/repository/maven-public/"
)
@Import(CustomStubRunnerConfiguration::class)
class ConsumerApplicationTests {

    @Autowired
    lateinit var restTemplate: RestTemplate

    @Test
    fun contextLoads() {
        val response = restTemplate.getForObject("/message", String::class.java)
        println(response)
        assertThat(response).isEqualTo("Hello from Producer")
    }
}

代码解释:

  1. CustomStubRunnerConfiguration: 一个 Spring Configuration 类,用于定义自定义的 Bean。
  2. customStubDownloader(...): 创建一个 StubDownloader Bean,并使用匿名类重写 downloadAndUnpackStubs 方法,实现自定义的 Stub 下载逻辑。
  3. @Import(CustomStubRunnerConfiguration::class):CustomStubRunnerConfiguration 导入到测试类中,使自定义的 Bean 生效。

5. 不同方法的比较

方法 优点 缺点 适用场景
自定义 Gradle 任务 简单易懂,配置清晰,不需要修改 Spring Cloud Contract 的内部实现。 需要手动管理 Stub 文件的下载和复制,与 Spring Cloud Contract 的集成度较低。 只需要在构建过程中下载 Stub 文件,不需要在测试运行时动态下载。
自定义 StubDownloader 灵活,可以完全控制 Stub 下载的行为,与 Spring Cloud Contract 的集成度较高。 需要编写额外的代码,理解 Spring Cloud Contract 的内部实现。 需要定制化 Stub 下载逻辑,例如,在下载后进行额外的处理,或者需要动态地改变下载目录。
StubRunnerConfiguration 最灵活,可以完全控制 StubRunner 的配置过程,适用于复杂的定制化需求。 相对复杂,需要深入理解 Spring Cloud Contract 的内部实现。 需要对 StubRunner 进行全面的定制,例如,需要替换默认的 StubDownloaderStubResolver

6. 总结与最佳实践

在 Gradle Kotlin DSL 中配置 Spring Cloud Contract 的 Stub 下载目录,主要有三种方法:自定义 Gradle 任务、自定义 StubDownloader 和使用 StubRunnerConfiguration。选择哪种方法取决于具体的应用场景和对定制化程度的需求。

最佳实践:

  • 选择合适的方法: 根据项目的复杂度和对定制化的需求,选择最合适的方法。对于简单的场景,可以使用自定义 Gradle 任务;对于需要高度定制化的场景,可以使用 StubRunnerConfiguration
  • 清晰的目录结构: 使用清晰的目录结构来组织 Stub 文件,方便查找和管理。
  • 版本控制: 将 Stub 文件纳入版本控制,方便回溯和协作。
  • 自动化测试: 将 Stub 下载和测试过程自动化,确保每次构建都能正确地下载和使用 Stub 文件。

希望今天的分享能帮助大家更好地理解 Spring Cloud Contract 在 Gradle Kotlin DSL 环境下的 Stub 下载目录配置,并在实际项目中应用起来。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注