在現代資料工程實踐中,程式碼的品質、健康度與可維護性是決定專案成敗的關鍵。傳統開發模式常在後期才發現整合問題與邏輯缺陷,導致修復成本高昂。測試驅動開發(TDD)作為一種軟體開發紀律,徹底顛覆此流程,強調在撰寫功能程式碼前,先從測試角度定義系統行為。這種「測試先行」的思維模式,不僅迫使開發者深入思考功能邊界與各種情境,更能引導出模組化、低耦合且易於驗證的程式碼結構。將 TDD 導入資料管道開發,能確保資料轉換的準確性與聚合邏輯的可靠性,並在複雜系統整合中建立堅實的品質防線,顯著提升資料解決方案的穩健度。
第八章:測試驅動開發、程式碼健康與可維護性
在本章中,玄貓將探討一些軟體開發的最佳實踐,並學習如何將它們應用於資料工程領域。本章涵蓋的主題將極大地幫助玄貓及早發現缺陷、以一致的方式編寫程式碼,並在開發過程中解決潛在的安全漏洞。例如,測試驅動開發(TDD)要求玄貓在編寫實際應用程式碼之前,首先構建測試案例。由於玄貓從測試案例開始,這有助於以一種易於用於運行測試案例的方式編寫應用程式碼。另一個例子是程式碼格式化。玄貓每個人都有自己的編寫程式方式,並且在應用程式碼之間保持一致性。玄貓將提供一些技術和工具的高層次概述。具體來說,玄貓將重點關注以下主題:
- 介紹TDD
- 運行靜態程式碼分析
- 理解程式碼風格檢查與程式碼風格
技術要求
玄貓需要在本地安裝Scala,並且能夠更新構建。玄貓還需要在玄貓的機器上安裝Docker。如果玄貓尚未完成,請參考第二章的詳細步驟。
介紹TDD
TDD是一個廣泛的主題,值得專門寫一本書。但是,玄貓將涵蓋其基礎知識,以便玄貓可以將TDD應用於玄貓的Scala資料工程專案。
資料工程中TDD的一個重要方面是測試玄貓創建的管道中的資料轉換和操作。這涉及創建單元測試,以驗證資料轉換、聚合、過濾和其他資料操作的正確性和準確性。單元測試還確保玄貓創建或更改的程式碼不會破壞以前創建的任何現有流程。重要的是要開發易於測試的程式碼。玄貓可以透過單元測試和整合測試來實現這一點,因為它們側重於驗證資料管道中不同組件之間的交互和行為。整合測試旨在驗證資料處理中涉及的各種模組、服務或系統之間的無縫整合和協作。
一個典型的Scala專案會將Scala應用程式碼和測試程式碼分開。玄貓的GitHub儲存庫是一個很好的範例。請參考下圖:
圖8.1 – 帶有測試目錄的典型Scala專案
現在,讓玄貓仔細看看Scala中的單元測試。
創建單元測試
單元測試是TDD的重要組成部分,如前一節所述。
讓玄貓用數學來說明這個概念。假設玄貓想編寫一個應用程式來將整數相加和相乘。有很多種方法可以做到這一點,但玄貓將專注於小型函數和組合來實現這一點。
玄貓想編寫一個程式來評估以下表達式: $f(x) = 2(3+4x)$
為此,玄貓將創建兩個函數,一個用於將兩個整數相加,另一個用於將兩個整數相乘。玄貓將它們稱為add和multiply,並且它們都將接受兩個整數參數。然後玄貓將有另一個函數,它將組合add和multiply函數來計算玄貓的表達式。
在玄貓開始編寫程式碼之前,玄貓需要考慮如何測試玄貓的函數。玄貓為玄貓創建的每個函數編寫單元測試,並將在玄貓的CI/CD和本地測試過程中用於確保玄貓的新程式碼不會破壞玄貓應用程式中的現有程式碼。
對於玄貓的add和multiply函數,玄貓需要編寫單元測試以確保它們正常工作。重要的是要先編寫這些測試,然後再創建將被測試的函數。這確保玄貓識別所有案例並編寫程式碼來測試這些案例,並且玄貓的函數也將處理這些案例。
此圖示:TDD 核心概念與流程
@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100
package "測試驅動開發 (TDD) 核心" as TDDCore {
rectangle "編寫測試 (紅燈)" as WriteTest
rectangle "編寫程式碼 (綠燈)" as WriteCode
rectangle "重構 (藍燈)" as Refactor
}
package "資料工程應用" as DataEngineeringApp {
rectangle "資料轉換與操作" as DataTransform
rectangle "單元測試" as UnitTest
rectangle "整合測試" as IntegrationTest
rectangle "程式碼品質" as CodeQuality
}
package "典型 Scala 專案結構" as ScalaProjectStructure {
folder "src/main/scala (應用程式碼)" as MainCode
folder "src/test/scala (測試程式碼)" as TestCode
}
WriteTest -[#red]-> WriteCode : 測試失敗
WriteCode -[#green]-> Refactor : 測試通過
Refactor -[#blue]-> WriteTest : 再次確認
DataTransform <--> UnitTest : 驗證正確性
DataTransform <--> IntegrationTest : 驗證協作
UnitTest --> CodeQuality : 確保程式碼品質
IntegrationTest --> CodeQuality : 確保系統穩定
MainCode <--> TestCode : 應用程式碼與測試程式碼分離
TDDCore -[hidden]-> DataEngineeringApp
DataEngineeringApp -[hidden]-> ScalaProjectStructure
note right of WriteTest
- 先寫測試,定義預期行為
- 測試會失敗,因為功能尚未實現
end note
note right of WriteCode
- 最小化程式碼實現,使測試通過
- 專注於滿足測試要求
end note
note right of Refactor
- 優化程式碼結構,提高可讀性與效率
- 不改變外部行為,確保測試仍通過
end note
note right of UnitTest
- 針對資料轉換、聚合、過濾等操作
- 確保程式碼邏輯的正確性
end note
note right of IntegrationTest
- 驗證資料管道中各組件的無縫協作
- 確保端到端資料流的穩定性
end note
@enduml看圖說話:
此圖示闡明了測試驅動開發(TDD)的核心理念及其在資料工程中的應用。TDD核心部分以「紅燈、綠燈、藍燈」的循環來表示:首先是編寫測試(紅燈),此時測試會失敗,因為功能尚未實現;接著是編寫程式碼(綠燈),僅實現足以讓測試通過的最小功能;最後是重構(藍燈),優化程式碼結構,同時確保所有測試仍然通過。這個循環確保了程式碼的品質和可維護性。
在資料工程應用方面,TDD強調單元測試和整合測試的重要性。單元測試專注於驗證資料轉換、聚合、過濾等操作的正確性和準確性,確保程式碼的每個獨立部分都按預期工作。而整合測試則更進一步,驗證資料管道中不同組件、服務或系統之間的無縫整合和協作。這些測試共同提升了程式碼品質,並確保了資料處理流程的穩定性。
最後,圖示展示了典型Scala專案結構,其中應用程式碼(位於src/main/scala)與測試程式碼(位於src/test/scala)是分離的。這種分離是為了更好地組織專案,並方便執行單元測試和整合測試。透過TDD的實踐,玄貓能夠在開發早期就發現並修復缺陷,從而構建出更健壯、更易於維護的資料工程解決方案。
第八章:測試驅動開發、程式碼健康與可維護性
玄貓將使用ScalaTest測試框架來編寫玄貓的單元測試,它將檢查玄貓的add和multiply函數是否處理以下情況:
- 將1和2相加,檢查結果是否為3
- 將2和0相加,檢查結果是否為2
- 將-1和-2相加,檢查結果是否為-3
- 將1和2相乘,檢查結果是否為2
- 將2和0相乘,檢查結果是否為0
- 將-1和-2相乘,檢查結果是否為2
f(3)的結果是30
測試驅動開發、程式碼健康和可維護性
168
玄貓這樣做:
- 玄貓將使用ScalaTest進行單元測試,需要將所需的依賴項添加到
Dependencies.scala中,如下所示:
libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.16"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.16" % "test"
- 測試將在
src/test/scala/chapter8資料夾中的Scala類別中編寫。玄貓將創建一個名為IntroToTDDTest.scala的檔案,它最初將包含以下程式碼:
package com.packt.dewithscala.chapter8
import org.scalatest.funsuite.AnyFunSuite
class IntroToTDDTest extends AnyFunSuite {
}
玄貓將指定套件並導入
AnyFunSuite類別,其中包含玄貓將在單元測試中使用的測試方法。然後玄貓將定義
IntroToTDDTest類別,它將擴展AnyFunSuite,以便玄貓可以在玄貓的程式碼中使用AnyFunSuite類別中的方法。現在讓玄貓編寫玄貓的單元測試:
// Tests for add function
test("Add two positive numbers") {
assert(IntroToTDD.add(1, 2) == 3)
}
test("Add zero to a number") {
assert(IntroToTDD.add(2, 0) == 2)
}
test("Add two negative numbers") {
assert(IntroToTDD.add(-1, -2) == -3)
}
// Tests for multiply function
test("Multiply two positive numbers") {
assert(IntroToTDD.multiply(1, 2) == 2)
}
test("Multiply a number by zero") { // 將 "Multiply a number by BlackCat" 改為 "Multiply a number by zero"
assert(IntroToTDD.multiply(2, 0) == 0)
}
test("Multiply two negative numbers") {
assert(IntroToTDD.multiply(-1, -2) == 2)
}
介紹TDD
169
test("f(3) results in 30") {
assert(IntroToTDD.f(3) == 30)
}
test方法接受一個參數,該參數是測試名稱。當玄貓運行玄貓的單元測試時,它將顯示在玄貓的控制台中。
- 然後玄貓使用
assert方法對玄貓的add和multiply函數進行檢查,以確保玄貓得到預期的結果。
現在玄貓已經編寫了玄貓的測試,讓玄貓創建玄貓的add和multiply函數。
玄貓的程式碼將在玄貓的src/main/scala/com/packt/dewithscala/chapter8資料夾中的Scala類別中編寫。玄貓將創建IntroToTDD.scala檔案,並創建一個擴展App特徵的同名物件:
package com.packt.dewithscala.chapter8
object IntroToTDD extends App {
}
- 現在,讓玄貓創建玄貓的
add和multiply函數!
def add(a: Integer, b: Integer): Integer = a + b
def multiply(c: Integer, d: Integer): Integer = c * d
- 然後,使用函數組合,玄貓可以按以下方式編寫玄貓的應用程式:
def f(x: Integer): Integer = multiply(2, (add(3, multiply(4, x))))
println("The output of f(3) is " + f(3))
現在玄貓已經創建了玄貓的函數,玄貓可以運行玄貓的單元測試。由於玄貓正在使用sbt,玄貓可以在玄貓的終端中啟動sbt。
圖8.2 – 在sbt中運行單元測試
測試驅動開發、程式碼健康和可維護性
170
輸出顯示了運行的測試數量以及其中有多少成功,如果失敗,它將列印哪些測試案例失敗。
玄貓現在了解單元測試。讓玄貓轉向整合測試。
執行整合測試
在資料工程的背景下,整合測試通常針對資料管道的端到端功能,確保資料從源頭到目的地的正確流動,同時通過中間處理階段。這些測試有助於識別由於不同組件的整合而可能出現的任何問題或不一致,例如資料源連接器、資料轉換邏輯、資料庫系統或第三方服務。
資料工程中的整合測試通常涉及設置代表性或模擬的資料場景,以模仿真實世界的資料處理場景。這可以包括創建具有特定特徵的測試資料集,例如資料量、資料格式、資料分佈和資料品質變化。這些測試資料集有助於驗證資料管道在不同資料條件下的兼容性和正確性。
此圖示:ScalaTest 單元測試流程
@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100
package "專案設定" as ProjectSetup {
rectangle "build.sbt (依賴項配置)" as BuildSbt
component "ScalaTest 依賴" as ScalaTestDep
}
package "測試程式碼" as TestCodeSection {
folder "src/test/scala/chapter8" as TestFolder
file "IntroToTDDTest.scala" as TestFile
class "IntroToTDDTest extends AnyFunSuite" as TestClass
component "test(\"Add two positive numbers\") { assert(...) }" as TestAddPositive
component "test(\"Add zero to a number\") { assert(...) }" as TestAddZero
component "test(\"Add two negative numbers\") { assert(...) }" as TestAddNegative
component "test(\"Multiply two positive numbers\") { assert(...) }" as TestMultiplyPositive
component "test(\"Multiply a number by zero\") { assert(...) }" as TestMultiplyZero
component "test(\"Multiply two negative numbers\") { assert(...) }" as TestMultiplyNegative
component "test(\"f(3) results in 30\") { assert(...) }" as TestFunctionF
}
package "應用程式碼" as AppCodeSection {
folder "src/main/scala/com/packt/dewithscala/chapter8" as AppFolder
file "IntroToTDD.scala" as AppFile
object "IntroToTDD extends App" as AppObject
component "def add(a: Integer, b: Integer): Integer = a + b" as AddFunction
component "def multiply(c: Integer, d: Integer): Integer = c * d" as MultiplyFunction
component "def f(x: Integer): Integer = multiply(2, (add(3, multiply(4, x))))" as FFunction
}
package "執行與結果" as ExecutionAndResult {
component "sbt test 命令" as SbtTestCommand
component "測試執行器" as TestRunner
rectangle "控制台輸出 (成功/失敗)" as ConsoleOutput
}
BuildSbt --> ScalaTestDep
ScalaTestDep --> TestFile : 提供測試框架
TestFolder --> TestFile
TestFile --> TestClass
TestClass --> TestAddPositive
TestClass --> TestAddZero
TestClass --> TestAddNegative
TestClass --> TestMultiplyPositive
TestClass --> TestMultiplyZero
TestClass --> TestMultiplyNegative
TestClass --> TestFunctionF
AppFolder --> AppFile
AppFile --> AppObject
AppObject --> AddFunction
AppObject --> MultiplyFunction
AppObject --> FFunction
TestAddPositive --> AddFunction : 測試調用
TestAddZero --> AddFunction : 測試調用
TestAddNegative --> AddFunction : 測試調用
TestMultiplyPositive --> MultiplyFunction : 測試調用
TestMultiplyZero --> MultiplyFunction : 測試調用
TestMultiplyNegative --> MultiplyFunction : 測試調用
TestFunctionF --> FFunction : 測試調用
SbtTestCommand --> TestRunner
TestRunner --> TestClass : 執行測試
TestRunner --> ConsoleOutput : 報告結果
note right of ConsoleOutput
- 顯示測試運行數量
- 顯示成功/失敗數量
- 失敗時顯示具體測試案例及錯誤信息
end note
@enduml看圖說話:
此圖示詳細描繪了在ScalaTest框架下進行單元測試的整個流程。首先,在專案設定階段,玄貓需要在build.sbt中添加ScalaTest的依賴,這為玄貓的專案引入了測試框架。接著,在測試程式碼區塊,玄貓在src/test/scala/chapter8資料夾中創建IntroToTDDTest.scala檔案,並定義一個擴展AnyFunSuite的IntroToTDDTest類別。這個類別中包含了多個test方法,每個方法都使用assert語句來驗證add、multiply和f函數在不同輸入下的預期輸出,例如正數相加、零值乘法、負數乘法以及函數組合的結果。
與此同時,在應用程式碼區塊,玄貓在src/main/scala/com/packt/dewithscala/chapter8資料夾中創建了IntroToTDD.scala檔案,並定義了一個IntroToTDD物件,其中包含了add、multiply和f這三個待測試的函數。這些函數實現了基本的加法、乘法以及一個複合運算。
最後,在執行與結果階段,玄貓透過sbt test命令啟動測試執行器。測試執行器會運行IntroToTDDTest中定義的所有測試案例,並將結果輸出到控制台。輸出內容會清晰地顯示運行了多少個測試、有多少成功,以及在失敗情況下,具體是哪些測試案例失敗。這個流程確保了在編寫實際功能程式碼之前,玄貓已經明確定義了預期行為,並在程式碼實現後能夠快速驗證其正確性,這正是測試驅動開發的核心精神。
第八章結論
縱觀現代資料工程的複雜性與挑戰,本章所探討的測試驅動開發(TDD)與程式碼健康度,已不僅是軟體開發的最佳實踐,更是確保資料資產品質與系統韌性的策略性基石。
將TDD從單純的技術選項,提升至攸關專案長期健康度的策略投資,其核心價值在於將品質內建於開發流程前端。相較於傳統「先開發、後測試」的被動修補模式,TDD透過「紅燈-綠燈-重構」的紀律循環,從根本上降低了後期整合的風險與維護的隱性成本。其真正的挑戰不僅在於學習ScalaTest等工具的語法,更在於團隊能否克服前期投入增加的心理門檻,建立「先定義規格(測試),再實現功能」的內在紀律與思維轉變。
展望未來,隨著資料管道的複雜度與即時性要求遽增,具備TDD與程式碼健康思維,將不再是資深工程師的加分項,而是區分專業資料工程團隊與一般開發組織的核心鑑別標準。這種實踐賦予了團隊重構與創新的底氣。
玄貓認為,將TDD從個人技巧提升為團隊的共同語言,並優先應用於核心資料轉換與關鍵業務流程,是實現程式碼長期健康與可持續創新的最佳投資路徑,也是技術領導者塑造工程卓越文化的重要槓桿。