[Day 18] 重构我们的测试程序码

随着我们专案功能的增加,虽然目前只有两个函数,但是我们的测试函数已经增加了不少。

为了减少我们未来阅读测试程序的痛苦,也为了提升未来整个专案的可维护度,我们可以开始重构我们的测试程序了。

抽出重复逻辑

观察我们的测试程序,我们可以发现前面建立测试资料库的部分,是不断重复的

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

写的过程中,我们也有持续的运作测试程序,以确保我们没有改错东西。

这样,我们的测试程序码重构就大功告成罗!


<<:  全域

>>:  CSS垂直置中

Day 27 axios-logout(html、javascript)

先上html的部分,logout则是把Local Storage清掉 再来是javascript的部...

Day 26 讨论 AI 深度学习论点

大家好~~欢迎来到第二十六篇 聊聊 AI 相关论点 本篇呢,来跟大家分享之前本人有做过一个跟车子有关...

Day21 React Styled-Components 元件自己的CSS

即时我们在不同元件分别引入CSS档,但打包後其实每个CSS还是会整个专案共用。 只想对单独元件设立自...

[Lesson13] OkHttp

添加 OkHttp 依赖库 要使用 OkHttp,必须在 gradle (Module) 层级的 d...

[Day6] 词性标注(一)-前言

一. 前言 词性标注 Part Of Speech(後面皆简称POS),简单来说就是将文章、句子中,...