有了前面的基礎,今天我們要在專案裡實作一個「購物車(ShoppingCart)」類別。為了確認實作符合預期的規格,我們將會以 TDD(Test-Driven Development)的風格來寫程式。換句話說,我們會用「先寫測試、再寫實際程式」的來回循環來完成這個類別的實作。最終的目標是,當我們把程式碼推到 GitHub 時,TeamCity 會自動拉取最新的程式碼變更並跑測試。若測試失敗的話,我們會收到 TeamCity 的通知並可以在 TeamCity 的 Build Log 裡看到所有歷程的紀錄。
在 2021 年的現代,寫測試的方式有千百種,測試框架也是多如牛毛。以 Kotlin 生態系來說,從老牌 的 JUnit 到後起新秀 Kotest 都有各自的風格與專長。剛開始接觸 Kotlin 的時候,筆者也是先從 JUnit 開始練習,畢竟 xUnit 系列概念大多是共通的,比較好上手。但隨著對 Kotlin 及相關測試工具的認識愈多,發現像 Kotest 這種揉和各家測試風格集大成的框架,其設計更符合 Kotlin 開發者喜歡的簡潔風格。在這個系列裡,筆者將用 Kotest 測試框架做示範。
為了要在練習專案裡使用 Kotest,我們要先在專案裡新增 Kotest 測試框架 ,這意味著我們要在 Gradle 的 Build Script(也就是在專案根目錄底下的 build.gradle.kts
)裡新增相依套件。
我們可以直接參考 Kotest 官網說明 在 build.gradle.kts
裡增加 kotestVersion
及 dependencies
設定:
val kotestVersion: String by project
dependencies {
// ...
testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
testImplementation("io.kotest:kotest-property:$kotestVersion")
}
其中 $kotestVersion
是我們設定的變數,做為我們統一儲存版本號的單一來源。這個變數我們會另外以 Key-Value 的型式存在 gradle.properties
設定檔裡。至於該用哪個 Kotest 版本?可以到 Maven Central 去查詢,以 io.kotest:kotest-runner-junit5
為例,目前最新穩定版是 4.6.2
。
kotestVersion=4.6.2
設定好 build.gradle.kts
及 gradle.properties
後,別忘了按一下右上角的 Load Gradle Changes 按鈕,IntelliJ IDEA 會重新整理(包括下載、更新、移除)所有的相依,並載入到專案內。
接著我們來新增我們要建立的類別。首先在 src/main/kotlin
底下新增 Package,對著資料夾按右鍵,然後選 New > Package 選單,以我自己的 Domain 為例,輸入 Package 名稱為 io.kraftsman
。
接著在我們新建的 Package 裡新增購物車類別。對著 Package 資料夾按右鍵,然後選 New > Kotlin Class/File,輸入類別名稱為 ShoppingCart
。IntelliJ IDEA 會在 src/main/kotlin/io.kraftsman
底下建立一個名為 ShoppingCart.kt
的檔案,並預先寫好 Boilerplate Code 如下:
package io.kraftsman
class ShoppingCart {
}
在 TDD 的最佳實踐裡,我們要先寫一個可以執行、但一定失敗的測試,確保我們的測試是真的能被執行,而且如預期的會失敗,這樣才不會因為所有設定都是 Happy Path 而有偏誤。我們可以用 IntelliJ IDEA 來幫我們建立測試類別,把游標放在 ShoppinCart
類別的兩個 { }
之間,然後按 ⌘+N
叫出 Generate 選單,選 Test。在彈出視窗裡把 Testing library 更換成 Kotest,其他保持預設值後按 OK。IntelliJ IDEA 會自動幫我們產生 ShoppingCartTest
類別在 src/test/io.kraftsman/
路徑底下,並預先寫好 Boilerplate Code 如下:
package io.kraftsman
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
class ShoppingCartTest : FunSpec({
})
接著我們可以直接點選類別名稱旁的綠色播放鍵,讓 IntelliJ IDEA 幫我們執行 Kotest 的測試。執行結果會出錯是正常的,因為我們本來就打算這樣錯。
接著我們要開始用寫測試的方式來描述我們預期這個 ShoppingCart
類別我們可以怎麼使用?首先我們可以先用一個 context()
函式來描述測試脈絡,接著再用 test()
以「測試 3A(Arrange、Act、Assert)」來描述測試的行為。程式碼如下:
class ShoppingCartTest : FunSpec({
context("一個購物車") {
test("當兩個 100 元商品相加時,總價為 200") {
// Arrange
val product1 = Product(id = 1, name = "Product 1", price = 100)
val product2 = Product(id = 1, name = "Product 1", price = 100)
val shoppingCart = ShoppingCart()
// Act
shoppingCart.add(product1)
shoppingCart.add(product2)
// Assert
shoppingCart.totalPrice() shouldBe 200
}
}
})
當這段程式碼寫好時,不用執行就知道編譯會失敗,因為 IntelliJ IDEA 已經用紅字標示出 Product
類別不存在、ShoppingCart
類別上沒有 add()
及 totalPrice()
等問題。這時也不用急著自己去修,我們可以讓 IntelliJ IDEA 快速地幫我們把決漏的程式碼「補」起來。
把游標放在這些被標記成紅字的錯誤程式碼上,然後按快速鍵 Option+Enter(macOS)
或 Alt+Enter(Windows)
,IntelliJ IDEA 會自動提示該怎麼修正,直接按 Enter 讓它幫我們產生 Product
類別及 ShoppingCart
類別的有 add()
及 totalPrice()
方法。
有 IntelliJ IDEA 幫我們寫好基本的程式碼「骨架」後,接下來我們只要「填肉」就好。首先打開 Product
類別,由於它只是一個承裝商品資訊的容器,在 Kotlin 裡可以直接把它宣告成 Data Class。先在 class
前加上 data
關鍵字,把所有建構子的參數加上 val
宣告,因為這個類別不需要邏輯,所以也把多餘的大括號去掉。修改後的程式碼長得會像這樣:
data class Product(val id: Int, val name: String, val price: Int)
在 ShoppingCart
類別裡,我們在內部參加一個儲存 Product
的 List,當使用者呼叫 add()
方法時就可以把 Product 加進 List 裡。而 totalPrice
就會回傳這個 List 裡所有商品 price 的總和。完成後的程式碼像這樣:
class ShoppingCart {
private val products = mutableListOf<Product>()
fun add(product: Product) {
products.add(product)
}
fun totalPrice(): Int {
return products.sumOf { it.price }
}
}
這時再回到 ShoppingCartTest
執行測試,假如您看到 Run 面板裡出現的是綠色勾勾的話,就表示剛剛增加的程式碼都是正確的,一個初步的購物車類別就開發完成了!
透過這種 TDD 的開發流程,我們就可以確保程式碼的設計是符合使用者的期待,之後再新增其他程式碼時,只要測試沒有出現紅色錯誤訊息,就表示我們的程式沒有被改壞,是不是更放心了呢?
明天我們就要來看看如何用 TeamCity 來執行 Kotest 的測試案例,以及若測試不通過時會有什麼反應?