隨著我們專案功能的增加,雖然目前只有兩個函數,但是我們的測試函數已經增加了不少。
為了減少我們未來閱讀測試程式的痛苦,也為了提升未來整個專案的可維護度,我們可以開始重構我們的測試程式了。
觀察我們的測試程式,我們可以發現前面建立測試資料庫的部分,是不斷重複的
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(Users)
SchemaUtils.create(Tags)
SchemaUtils.create(UsersTags)
// ...
我們可以將這段邏輯獨立成一個函數 initDatabase()
private fun initDatabase() {
Database.connect(
"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;",
driver = "org.h2.Driver"
)
transaction {
SchemaUtils.create(Users)
SchemaUtils.create(Tags)
SchemaUtils.create(UsersTags)
}
}
由於我們有加上 DB_CLOSE_DELAY=-1;
這段參數,我們不用擔心資料庫建立在切換 transaction()
時被重設,其他 transaction()
都可以使用我們建立的資料表。
然後原本的測試就可以透過 initDatabase()
執行這段邏輯,比方說昨天的 測試單一全新用戶加上標籤
就可以變成
fun `測試單一全新用戶加上標籤`() {
initDatabase()
transaction {
val testUser = User.new {
name = "TestUser"
}
val testTag = Tag.new {
name = "TestTag"
}
updateUsersTags(listOf(testUser), listOf(testTag))
assertEquals(
listOf(testTag),
testUser.tags.toList()
)
}
}
是不是比起昨天更簡潔了一點點呢?
改好了之後,記得執行一下測試,看看我們有沒有哪邊改錯了。
如果改錯的話,由於我們修改的幅度很小,我們只要趕快復原成修改前的樣子,再看看改錯的地方可能是哪邊,這樣就可以安心地繼續往下重構了。
改好並通過測試之後,我們再往下看看還有哪邊可以調整。
再來,我們觀察到我們的測試都會嘗試建立幾個 User
物件以及 Tag
物件,來進行一系列的操作。
當我們需要很多個物件時,就會導致我們要執行很多次的 User.new()
或 Tag.new()
。
有沒有辦法將這段邏輯抽離出去呢?當然可以!我們來建立一個 makeTestUsers()
函數,接收 num
來設定我們要做出幾個 User
物件,並回傳 List<USer>
private fun makeTestUsers(num: Int): List<User> {
}
接著就是建立邏輯了,我們觀察到 List()
這個函數的簽名
public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T>
我們發現到,第一個參數是這個 List
的大小,第二個參數就是每個物件初始化時,所使用的 lambda 函數。這個跟我們需要的邏輯不謀而合!
我們先寫 makeTestUsers()
的第一個版本
private fun makeTestUsers(num: Int): List<User> {
return List(size=num){ User.new { name="TestUser" }}
}
再來我們發現,這個函數只有單行!在 Kotlin 語法下可以更簡化
private fun makeTestUsers(num: Int): List<User> =
List(size=num){ User.new { name="TestUser" } }
以同樣的邏輯,我們可以做出 makeTestTags()
來建立測試用的 List<Tag>
private fun makeTestTags(num: Int): List<Tag> =
List(size=num){ Tag.new { name="TestTag" } }
然後,我們就能在測試程式內快速建立很多 User
和 Tag
物件了
val testUsers = makeTestUsers(2)
不過我們後來發現,這樣的寫法有個缺點,會導致我們在存取單一物件時,需要用很多的 testUsers[0]
、testUsers[1]
來標記我們的物件,這樣看起來有點不美觀。
有沒有辦法讓這段邏輯看起來,再更加的直觀一點呢?
Kotlin 支援在宣告物件時,就直接將 List
解構的寫法。
例如
val (testUser, testUser2) = makeTestUsers(2)
就會讓 testUSer
和 testUser2
是透過 makeTestUsers()
建立的 User
物件了。
並且這個寫法也支援只有一個元素的 List
解構
val (testUser) = makeTestUsers(1)
利用這個語法,我們可以讓我們的測試,再更加的精簡
fun `測試單一全新用戶加上標籤`() {
initDatabase()
transaction {
val (testUser) = makeTestUsers(1)
val (testTag) = makeTestTags(1)
updateUsersTags(listOf(testUser), listOf(testTag))
assertEquals(
listOf(testTag),
testUser.tags.toList()
)
}
}
其他的測試案例,也可以照相同邏輯進行簡化
fun `測試多個用戶加上標籤`() {
initDatabase()
transaction {
val (testUser, testUser2) = makeTestUsers(2)
val (testTag) = makeTestTags(1)
updateUsersTags(listOf(testUser, testUser2), listOf(testTag))
assertEquals(
listOf(testTag),
testUser.tags.toList()
)
assertEquals(
listOf(testTag),
testUser2.tags.toList()
)
}
}
簡化過之後,記得要再執行一下測試,以確保沒有任何東西被改壞喔!
assertThat()
昨天我們在檢查 List 不應存在某個元素時,我們用到了 assertThat()
這個函數
assertThat(testUser.tags.toList(), not(hasItem(oldTag)))
其實之前我們所撰寫的 assertEquals()
邏輯,也可以用 assertThat()
實作,並且語意會更加清楚!
我們先來看看 assertThat()
的簽名
public static <T> void assertThat(T actual, Matcher<? super T> matcher)
第一個參數是被測試的資料,第二個是我們希望成立的條件。
以前面的 assertEquals(listOf(testTag),testUser2.tags.toList())
舉例,就是我們希望 testUser2.tags.toList()
等於 listOf(testTag)
。
用 matcher
的角度來設計,我們可以用 is()
這個函數來實作。不過由於 is
是一個 Kotlin 的關鍵字,所以我們要用 「`」將 is
包起來。
看起來像是這樣
assertThat(testUser.tags.toList(),`is`(listOf(testTag)))
透過這樣的調整,整段程式看起來更簡潔清楚了。簡直就可以直接唸出來:「assert that testUser's tags is list of testTag」
我們來看看新的 測試單一全新用戶加上標籤
fun `測試單一全新用戶加上標籤`() {
initDatabase()
transaction {
val (testUser) = makeTestUsers(1)
val (testTag) = makeTestTags(1)
updateUsersTags(listOf(testUser), listOf(testTag))
assertThat(
testUser.tags.toList(),
`is`(listOf(testTag))
)
}
}
測試多個用戶加上標籤
fun `測試多個用戶加上標籤`() {
initDatabase()
transaction {
val (testUser, testUser2) = makeTestUsers(2)
val (testTag) = makeTestTags(1)
updateUsersTags(listOf(testUser, testUser2), listOf(testTag))
assertThat(
testUser.tags.toList(),
`is`(listOf(testTag))
)
assertThat(
testUser2.tags.toList(),
`is`(listOf(testTag))
)
}
}
寫的過程中,我們也有持續的運作測試程式,以確保我們沒有改錯東西。
這樣,我們的測試程式碼重構就大功告成囉!