第二条:使用TDD方法开发Gradle插件
Publié le 24 September 2025
在本文中,我们将探讨如何使用测试驱动开发(TDD - Test-Driven Development)的方法为 Gradle 插件建立一个坚实的开发基础。 这种方法确保我们的代码是健壮、可维护的,并且能够从一开始就精确满足需求。
项目初始化
Gradle 通过命令大大简化了创建新插件的过程。gradle init. 选择使用 Kotlin 创建一个 "Gradle Plugin",我们将得到一个即用型项目结构,其中包含两种关键的测试类型:
-
单元测试 :位于`src/test`, 它们允许验证我们插件的隔离组件,例如任务的内部逻辑或扩展的配置。它们速度快,不需要完整的 Gradle 执行。
-
功能测试:位于`src/functionalTest`, 他们使用`GradleRunner`为了在临时测试项目中执行完整的Gradle构建。这使我们能够在受控环境中验证插件的实际行为。
2. 我们的首个 TDD 周期:记录一项任务
我们的第一个要求很简单:插件必须记录一个已命名的任务。printSiteConfig。
2.1. 首先测试(失败的测试)
根据 TDD,我们首先编写一个测试来验证该任务的存在。在`SiteBakerPluginTest.kt`(我们的单元测试文件),我们添加:
@Test
fun `plugin registers task`() {
// Créer un projet de test en mémoire
val project = ProjectBuilder.builder().build()
project.plugins.apply("com.cheroliv.site-baker")
// Vérifier que la tâche a bien été enregistrée
assertNotNull(project.tasks.findByName("printSiteConfig"))
}
此测试失败,因为我们尚未在插件中编写任何代码。
2.2. 代码随后(使测试通过)
现在,我们写所需的最少代码在`SiteBakerPlugin.kt`让测试通过:
class SiteBakerPlugin: Plugin<Project> {
override fun apply(project: Project) {
// Enregistrer une tâche simple
project.tasks.register("printSiteConfig") { task ->
// ... la logique de la tâche viendra plus tard
}
}
}
我们重新运行测试,它们通过了。我们的第一个功能已被验证。
3. TDD 第二阶段:添加 DSL 扩展
接下来的要求是允许用户通过一个 DSL 块在其配置我们的插件`build.gradle.kts`. 我们想要一个块`site { … }`可以指定配置路径的地方。
3.1. 首先测试
我们添加一个测试来验证扩展`site`它已正确记录,且可为其赋予一个值。
@Test
fun `plugin registers extension`() {
val project = ProjectBuilder.builder().build()
project.plugins.apply("com.cheroliv.site-baker")
// Récupérer l'extension et lui affecter une valeur
project.extensions
.findByType(SiteExtension::class.java)!!
.configPath
.set("config.yml")
// Vérifier que la valeur a bien été prise en compte
assertEquals(
"config.yml",
project.extensions.findByType(SiteExtension::class.java)?.configPath?.get()
)
}
此测试失败,因为既不该类`SiteExtension`也不存在扩展的注册
3.2. 接下来的代码
我们正在创建该类`SiteBakerExtension.kt`和我们更新`SiteBakerPlugin.kt`:
// SiteBakerExtension.kt
open class SiteExtension @Inject constructor(objects: ObjectFactory) {
val configPath: Property<String> = objects.property(String::class.java)
}
// SiteBakerPlugin.kt
class SiteBakerPlugin: Plugin<Project> {
override fun apply(project: Project) {
// Enregistrer l'extension
val extension = project.extensions.create("site", SiteExtension::class.java)
project.tasks.register("printSiteConfig") { task ->
task.doLast {
// On utilisera l'extension plus tard
}
}
}
}
�测试再次通过。
4. 功能测试:验证集成
既然单元已经测试完毕,我们必须确保它们在真实构建中能够协同工作。这就是功能测试的作用在`SiteBakerPluginFunctionalTest.kt`。
4.1. 集成测试:创建受控环境
这个测试将模拟使用我们插件的真实项目。为了让它可靠,它必须密封的, 换句话说,它不应依赖于系统上已存在的文件。它必须自己创建执行所需的所有条件。
�测试将因此 :
-
创建一个`build.gradle.kts`使用我们的插件及其DSL的测试
-
创建配置文件(
managed-jbake-context.yml) 插件应该读取的内容。这是测试稳健性的一步关键步骤。 -
执行任务`printSiteConfig`经过`GradleRunner`.
-
� 检查构建输出是否正确。
通过在每次执行时复制或创建此配置文件,我们确保测试是可重复并且不依赖于外部状态。这可以保护我们的测试免受与文件读取相关的回归问题的影响。
@Test fun `can run task with DSL`() {
// 1. Créer un build.gradle.kts de test
buildFile.writeText("""
plugins { id("com.cheroliv.site-baker") }
site { configPath = "managed-jbake-context.yml" }
""".trimIndent())
// 2. Créer le fichier de configuration pour un test contrôlé
val configFile = File(projectDir, "managed-jbake-context.yml")
configFile.writeText("site: { title: 'Mon Site de Test' }") // Contenu YAML simple
// 3. Exécuter la build
val runner = GradleRunner.create()
runner.withPluginClasspath()
runner.withArguments("printSiteConfig")
runner.withProjectDir(projectDir) // Spécifier le répertoire du projet de test
val result = runner.build()
// 4. Vérifier la sortie
assertTrue(result.output.contains("Site config path: managed-jbake-context.yml"))
}
此测试将在任务未完成时一直失败`printSiteConfig`它不会真的使用扩展的值。
4.2. 完成逻辑
我们正在更新任务在`SiteBakerPlugin.kt`为了使它显示配置的值 :
project.tasks.register("printSiteConfig") { task ->
task.doLast {
println("Site config path: ${extension.configPath.get()}")
}
}
所有测试,单元测试和功能测试,现在都通过了。
结论
采用TDD方法,我们以增量且安全的方式构建了一个插件。每个小功能都会立即通过测试进行验证,从快速的单元测试到验证完整集成的功能测试。
通过确保我们的测试是功能性的密封的— 例如,通过以编程方式创建必要的配置文件 — — 我们构建了一个极其健壮的安全网。这种严谨性能够有效防止回归,并使我们有足够信心随后添加更复杂的功能,如解析 YAML 配置文件。
这个坚实的测试基础是维护和插件未来发展最宝贵的资产。