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

Bài viết liên quan