Article 2 : Developing a Gradle Plugin with a TDD Approach
Published on 24 September 2025
In this article, we will explore how to set up a solid development base for a Gradle plugin using a Test-Driven Development (TDD) approach. This method ensures that our code is robust, maintainable, and precisely meets the requirements from the start.
1. Project Initialization
Gradle greatly facilitates the creation of a new plugin thanks to the command`gradle init`. By choosing to create a "Gradle Plugin" with Kotlin, we get a ready-to-use project structure, including two crucial types of tests:
-
Unit Tests:Located in`src/test`, they allow for the validation of isolated components of our plugin, such as the internal logic of a task or the configuration of an extension. They are fast and do not require a full Gradle execution.
-
Functional Tests:Located in`src/functionalTest`, they use`GradleRunner`to execute a complete Gradle build in a temporary test project. This allows us to verify the actual behavior of the plugin in a controlled environment.
2. Our First TDD Cycle: Registering a Task
Our first requirement is simple: the plugin must register a task named`printSiteConfig`.
2.1. Test First (The Failing Test)
Following TDD, we first write a test that verifies the existence of this task. In`SiteBakerPluginTest.kt`(our unit test file), we add:
@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"))
}
This test fails, as we have not yet written any code in our plugin.
2.2. Code Next (Making the Test Pass)
Now, we write the minimum amount of code necessary in`SiteBakerPlugin.kt`so that the test passes:
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
}
}
}
We run the tests again, and they pass. Our first feature is validated.
3. Second TDD Cycle: Adding a DSL Extension
The next requirement is to allow users to configure our plugin via a DSL block in their`build.gradle.kts`. We want a`site { … }`block where a configuration path can be specified.
3.1. Test First
We add a test to verify that the`site`extension is correctly registered and that a value can be assigned to it.
@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()
)
}
This test fails because neither the`SiteExtension`class nor the extension registration exists.
3.2. Code Next
We create the`SiteBakerExtension.kt`class and update`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
}
}
}
}
The tests pass again.
4. Functional Test: Validating Integration
Now that the units are tested, we must ensure that everything works together in a real build. This is the role of the functional test in`SiteBakerPluginFunctionalTest.kt`.
4.1. Integration Test: Creating a Controlled Environment
This test will simulate a real project using our plugin. To be reliable, it must behermetic, meaning it must not depend on existing files on the system. It must create all the conditions necessary for its execution itself.
The test will therefore:
-
Create a`build.gradle.kts`test project that uses our plugin and its DSL.
-
Create the configuration file(
managed-jbake-context.yml) that the plugin is supposed to read. This is a crucial step for the robustness of the test. -
Execute the`printSiteConfig`task via`GradleRunner`.
-
Verify that the build output is correct.
By copying or creating this configuration file at each execution, we ensure that the test isreproducibleand does not depend on an external state. This secures our tests against regressions that could be linked to file reading.
@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"))
}
This test will fail as long as the`printSiteConfig`task does not actually use the extension value.
4.2. Finalizing the Logic
We update the task in`SiteBakerPlugin.kt`so that it displays the configured value:
project.tasks.register("printSiteConfig") { task ->
task.doLast {
println("Site config path: ${extension.configPath.get()}")
}
}
All tests, unit and functional, now pass.
Conclusion
By following a TDD approach, we have built a plugin incrementally and securely. Each small feature is immediately validated by a test, from fast unit tests to functional tests that validate the complete integration.
By taking care to make our functional testshermetic— notably by programmatically creating the necessary configuration files — we build an extremely robust safety net. This rigor effectively protects us against regressions and gives us great confidence to add more complex features later, such as parsing the YAML configuration file.
This base of solid tests is the most valuable asset for the future maintenance and evolution of the plugin.