iT邦幫忙

2021 iThome 鐵人賽

DAY 18
1

隨著我們專案功能的增加,雖然目前只有兩個函數,但是我們的測試函數已經增加了不少。

為了減少我們未來閱讀測試程式的痛苦,也為了提升未來整個專案的可維護度,我們可以開始重構我們的測試程式了。

抽出重複邏輯

觀察我們的測試程式,我們可以發現前面建立測試資料庫的部分,是不斷重複的

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" } }

然後,我們就能在測試程式內快速建立很多 UserTag 物件了

val testUsers = makeTestUsers(2)

不過我們後來發現,這樣的寫法有個缺點,會導致我們在存取單一物件時,需要用很多的 testUsers[0]testUsers[1] 來標記我們的物件,這樣看起來有點不美觀。

有沒有辦法讓這段邏輯看起來,再更加的直觀一點呢?

Destructuring Declaration

Kotlin 支援在宣告物件時,就直接將 List 解構的寫法。

例如

val (testUser, testUser2) = makeTestUsers(2)

就會讓 testUSertestUser2 是透過 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))
		)  
    }  
}

寫的過程中,我們也有持續的運作測試程式,以確保我們沒有改錯東西。

這樣,我們的測試程式碼重構就大功告成囉!


上一篇
[Day 17] 新功能的測試,檢驗不應該存在的資料
下一篇
[Day 19] 突如其來的需求變更!來聊函數式編程
系列文
Kotlin 怎麼操作資料庫?談談 Kotlin Exposed 框架30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言