TDD của một plugin Gradle Kotlin có thể cấu hình với JBake và JGit
Đăng ngày 09 July 2025
Bài viết này trình bày quy trình thiết kế và kiểm thử (TDD) hoàn chỉnh cho một plugin Gradle được viết bằng Kotlin DSL. Plugin này tự động hóa việc tạo các trang tĩnh với JBake và triển khai Git thông qua JGit, tất cả đều dựa trên một tệp cấu hình YAML.
Mục tiêu
Tạo một plugin Gradle:
-
có thể cấu hình bằng Kotlin DSL ;
-
có khả năng:
-
tạo một trang web tĩnh từ JBake;
-
triển khai lên một kho lưu trữ Git từ xa thông qua JGit;
-
có thể tích hợp vào workflow CI/CD;
-
được kiểm thử theo quy trình TDD.
Cấu hình khai báo
Dưới đây là một ví dụ về cấu hình YAML để mô hình hóa trong DSL của chúng tôi:
bake:
srcPath: "./site/jbake"
destDirPath: "bake"
pushPage:
from: "bake"
to: "cvs"
repo:
name: "trainings"
repository: "https://github.com/pages-content/trainings.git"
credentials:
username: "USERNAME"
password: "SECRET_TOKEN"
branch: "main"
message: "cheroliv.com"
Cấu hình này được thể hiện thông qua một extension Gradle có thể khai báo bằng Kotlin DSL.
Kiến trúc của plugin
@startuml
package "engine-plugin" {
class WorkspaceEngineExtension
class GenerateSiteTask
interface GitAdapter
class JGitAdapter
WorkspaceEnginePlugin --> WorkspaceEngineExtension
WorkspaceEnginePlugin --> GenerateSiteTask
GenerateSiteTask --> GitAdapter
JGitAdapter ..|> GitAdapter
}
@enduml
Định nghĩa về phần mở rộng DSL
open class WorkspaceEngineExtension {
var outputDir: String = "build/site"
var template: String = "freemarker"
var repoUrl: String = ""
var branch: String = "main"
var message: String = "Generated by engine"
}
Định nghĩa tác vụ tùy chỉnh
abstract class GenerateSiteTask : DefaultTask() {
lateinit var gitAdapter: GitAdapter
@get:Input abstract val repoUrl: Property<String>
@get:InputDirectory abstract val outputDir: DirectoryProperty
@get:Input abstract val branch: Property<String>
@get:Input abstract val message: Property<String>
@TaskAction
fun run() {
val repo = gitAdapter.cloneRepository(repoUrl.get(), outputDir.get().asFile)
gitAdapter.commitAndPush(repo, branch.get(), message.get())
}
}
Interface GitAdapter
interface GitAdapter {
fun cloneRepository(uri: String, directory: File): Repository
fun commitAndPush(repo: Repository, branch: String, message: String)
}
Triển khai cụ thể với JGit
class JGitAdapter : GitAdapter {
override fun cloneRepository(uri: String, directory: File): Repository {
return Git.cloneRepository()
.setURI(uri)
.setDirectory(directory)
.call()
.repository
}
override fun commitAndPush(repo: Repository, branch: String, message: String) {
Git(repo).use {
it.add().addFilepattern(".").call()
it.commit().setMessage(message).call()
it.push().call()
}
}
}
DSL Cấu hình Kotlin
workspaceEngine {
outputDir = "build/out"
template = "freemarker"
repoUrl = "https://github.com/cheroliv/trainings.git"
branch = "main"
message = "deploy from plugin"
}
Kiểm thử đơn vị với Mockito
Dependencies
testImplementation("org.mockito:mockito-core:5.12.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
Kiểm tra hành vi Git
class GenerateSiteTaskMockitoTest {
@TempDir
lateinit var tempDir: File
@Test
fun `task should call clone and commitAndPush`() {
val project = ProjectBuilder.builder().build()
val task = project.tasks.create("generateSite", GenerateSiteTask::class.java)
val mockGitAdapter = mock<GitAdapter>()
val mockRepo = mock<Repository>()
whenever(mockGitAdapter.cloneRepository(any(), any())).thenReturn(mockRepo)
task.gitAdapter = mockGitAdapter
task.repoUrl.set("https://github.com/cheroliv/test.git")
task.outputDir.set(project.layout.projectDirectory.dir(tempDir.name))
task.branch.set("main")
task.message.set("test commit")
task.run()
verify(mockGitAdapter).cloneRepository(eq("https://github.com/cheroliv/test.git"), any())
verify(mockGitAdapter).commitAndPush(eq(mockRepo), eq("main"), eq("test commit"))
}
}
@startuml actor "Nhà phát triển" as Dev participant "Gradle ProjectBuilder" as Project participant "GenerateSiteTask" as Task participant "GitAdapter (Giả lập)" as Adapter participant "JGitAdapter / Mockito" as Impl Dev -> Project : xây dựng dự án Gradle Dev -> Task : Cấu hình các thuộc tính Task -> Adapter : cloneRepository() Adapter -> Impl : được mô phỏng bởi Mockito Task -> Adapter : commitAndPush() @enduml
Kết luận
Việc phân tách trách nhiệm thông qua`GitAdapter`cho phép áp dụng đầy đủ TDD trong quá trình phát triển plugin Gradle:
-
các lệnh gọi Git được trừu tượng hóa và có thể kiểm thử được;
-
cấu hình DSL sạch sẽ và rõ ràng;
-
plugin vẫn độc lập với triển khai thực tế.
Cách tiếp cận này đảm bảo khả năng kiểm thử và khả năng mở rộng cao (hướng tới GitHub API, GitLab, v.v.).
Các bước tiếp theo
-
Tích hợp việc tạo JBake như một`BakeAdapter`
-
Thêm các bài kiểm tra tích hợp với`GradleRunner`
-
Hỗ trợ tự động tải tệp lên`site.yml`via SnakeYAML