在我以往的Android開發生涯中,幾乎沒有使用過單元測試,也沒有見過有人去介紹過,好像這個東西在國内開發者眼裡并不是很重要,或者說大多數開發同學沒有專門的時間去使用單元測試架構,也許更重要的原因應該是我個人的孤陋寡聞。
背景
什麼是單元測試?
單元測試是針對最小的單元編寫測試代碼。在 Java 中,最小的功能機關是方法,是以,對Java 程式進行單元測試就是針對單個 Java 方法的測試。
為什麼要做單元測試
在國外,實際開發流程往往是,先編寫測試,測試寫完後,再開始真正編寫實作代碼。在具體實作過程中,一邊寫一邊測,什麼時候測試全部通過,就代表開發任務完成。這也就是我們常說的 TDD(
測試驅動開發
)
簡介
Junit
是一個開源的Java語言的單元測試架構,專門為 Java 設計,使用也最為廣泛。(當然 Kotlin 使用也沒問題,注意一些小細節即可)
依賴方式
Maven
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
複制
Gradle
dependencies {
testImplementation 'junit:junit:4.12'
}
複制
主要方法
Assert類中主要方法如下:
方法名 | 方法描述 |
---|---|
assertEquals | 斷言傳入的預期值與實際值是相等的 |
assertNotEquals | 斷言傳入的預期值與實際值是不相等的 |
assertArrayEquals | 斷言傳入的預期數組與實際數組是相等的 |
assertNull | 斷言傳入的對象是為空 |
assertNotNull | 斷言傳入的對象是不為空 |
assertTrue | 斷言條件為真 |
assertFalse | 斷言條件為假 |
assertSame | 斷言兩個對象引用同一個對象,相當于“==” |
assertNotSame | 斷言兩個對象引用不同的對象,相當于“!=” |
assertThat | 斷言實際值是否滿足指定的條件 |
注意
上面的所有方法,都有對應的重載方法,可以在前面加一個 String 類型的參數,表示斷言失敗時的提示。
常用注解
執行順序:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass
注解名 | 含義 |
---|---|
@Test | 表示此方法為測試方法 |
@Before | 在每個測試方法前執行,可做初始化操作 |
@After | 在每個測試方法後執行,可做釋放資源操作 |
@Ignore | 忽略的測試方法 |
@BeforeClass | 在類中所有方法前運作。此注解修飾的方法必須是static void |
@AfterClass | 在類中最後運作。此注解修飾的方法必須是static void |
@RunWith | 指定該測試類使用某個運作器 |
@Parameters | 指定測試類的測試資料集合 |
@Rule | 重新制定測試類中方法的行為 |
@FixMethodOrder | 指定測試類中方法的執行順序 |
使用方式
基礎使用
比如我們有一個等效括号的這樣一個算法。
StackExample.kt
/** 等效括号
* 如題:給定一個字元串所表示的括号序列,包含以下字元: '(', ')', '{', '}', '[' and ']', 判定是否是有效的括号序列。
* 括号必須依照 "()" 順序表示, "()[]{}" 是有效的括号,但 "([)]" 則是無效的括号。
*
* 解法思路:
* 使用棧存儲,将字元串切割為char周遊,先存儲指定方向的符号,如'(','{','['。
* 如果屬于右方向的,如'}'等,進入判斷,如果棧頂符号與目前char相等并且棧不會null,即為正确,否則直接return false
* */
fun isBrackets(str: String): Boolean {
if (str.length < 2) return false
val stack = Stack<Char>()
str.forEach {
if ("{([".contains(it)) {
stack.push(it)
} else {
if (stack.isNotEmpty() && isBracketChart(stack.peek(), it)) {
stack.pop()
} else return false
}
}
return stack.isEmpty()
}
private fun isBracketChart(str1: Char, str2: Char): Boolean =
(str1 == '{' && str2 == '}') ||
(str1 == '[' && str2 == ']') || (str1 == '(' && str2 == ')')
複制
代碼測試(未使用Junit)
如果是沒有使用 Junit,我們可能會寫出下面這樣的測試代碼:
fun main() {
println(isBrackets("{}"))
xxxx...
}
複制
相比來說我們如果我們增加别的方法,就需要頻繁修改main()方法,而且對于測試的正确性也不能做到直覺。
使用Junit
我們在相應的test包下,建立
StackExampleKtTest
這樣的類,或者直接使用如下快捷方式,在相應的方法前使用mac(option+回車),windows(ctrl+回車),如圖所示
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwIjNx8CX39CXy8CXycXZpZVZnFWbp9zZlBnauE2N4QDMmhDMzEGZmFTZ1cjN5kDNmdTOyQWOiFWO3UGNvwFMxEDN5ATMtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.jpeg)
示例如下:
StackExampleKtTest
class StackExampleKtTest {
@Test
fun testIsBrackets() {
assertArrayEquals(
booleanArrayOf(
isBrackets(""),
isBrackets("{"),
isBrackets("{{}}"),
isBrackets("{({})}"),
isBrackets("{({}"),
), booleanArrayOf(
false, false, true, true,false
)
)
}
}
複制
參數化測試
上述使用方法,如果我們每次測試一個方法都要去設定對應的值,相對比較繁瑣,那如何用連續不同的值去測試同一個方法呢,這樣就可以避免我們不去多次修改,節省部分時間。這時就要使用
@RunWith
和
@Parameters
.
首先需要在測試類上添加
RunWith(Paramterized.class)
注解,在建立一個由
@Paramters
注解的 static 方法,讓傳回一個對應的測試資料合集,最後建立構造方法,方法的參數順序和測試資料集合一一對應。
示例如下:
@RunWith(Parameterized::class)
class StackExampleKtTest(private val str: Pair<String, Boolean>) {
// TODO: 2020/11/15 相應的,這種處理方式也容易造成對錯誤的難以尋找
companion object {
@Parameterized.Parameters
@JvmStatic
fun primeStrings(): Collection<Pair<String, Boolean>> =
listOf(
"" to false,
"{" to false,
"{}" to false,
"{[]}" to true,
"asd{()}" to false
)
}
@Test
fun testIsBrackets() {
//注意這裡的錯誤提示
assertEquals("出錯了-目前 \"${str.first}\" to ${str.second}",
str.second, isBrackets(str.first))
}
}
複制
注意:相應的 @Parameterized.Parameters 方法在Kotlin中使用需要增加
@JvmStatic
。使用過程中,這種參數化測試如果我們沒有加
錯誤提示
,尋找問題時可能不容易找到那個測試用例出了問題,是以這點也需要注意。
assertThat用法
用于為斷言失敗後的輸出資訊提高可讀性。預設情況下,斷言失敗隻會抛出
AssertionError
,我們無法知道到底是哪裡出錯,而
assertThat
的作用就是解決這個問題。
常用的比對器整理:
比對器 | 說明 | 例子 |
---|---|---|
is | 斷言參數等于後面給出的比對表達式 | assertThat(5, is (5)); |
not | 斷言參數不等于後面給出的比對表達式 | assertThat(5, not(6)); |
equalTo | 斷言參數相等 | assertThat(30, equalTo(30)); |
equalToIgnoringCase | 斷言字元串相等忽略大小寫 | assertThat(“Ab”, equalToIgnoringCase(“ab”)); |
containsString | 斷言字元串包含某字元串 | assertThat(“abc”, containsString(“bc”)); |
startsWith | 斷言字元串以某字元串開始 | assertThat(“abc”, startsWith(“a”)); |
endsWith | 斷言字元串以某字元串結束 | assertThat(“abc”, endsWith(“c”)); |
nullValue | 斷言參數的值為null | assertThat(null, nullValue()); |
notNullValue | 斷言參數的值不為null | assertThat(“abc”, notNullValue()); |
greaterThan | 斷言參數大于 | assertThat(4, greaterThan(3)); |
lessThan | 斷言參數小于 | assertThat(4, lessThan(6)); |
greaterThanOrEqualTo | 斷言參數大于等于 | assertThat(4, greaterThanOrEqualTo(3)); |
lessThanOrEqualTo | 斷言參數小于等于 | assertThat(4, lessThanOrEqualTo(6)); |
closeTo | 斷言浮點型數在某一範圍内 | assertThat(4.0, closeTo(2.6, 4.3)); |
allOf | 斷言符合所有條件,相當于&& | assertThat(4,allOf(greaterThan(3), lessThan(6))); |
anyOf | 斷言符合某一條件,相當于或 | assertThat(4,anyOf(greaterThan(9), lessThan(6))); |
hasKey | 斷言Map集合含有此鍵 | assertThat(map, hasKey(“key”)); |
hasValue | 斷言Map集合含有此值 | assertThat(map, hasValue(value)); |
hasItem | 斷言疊代對象含有此元素 | assertThat(list, hasItem(element)); |
@Rule
在測試過程中,我們也可以通過增加
@Before
或者
@After
進而做到測試前後的一個提示效果,但是每次都這樣寫也許有點麻煩。是以這個時候可以使用
@Rule
.
示例如下:
TestPrompt
class TestPormpt : TestRule {
override fun apply(statement: Statement, description: Description): Statement {
// 擷取測試方法的名字
val methodName: String = description.methodName
//相當于 @Before
println(methodName + "測試開始前!")
// 運作的測試方法
statement.evaluate()
//運作結束,相當于@After
println(methodName + "測試結束!")
return statement
}
}
複制
class StackExampleKtTest {
// TODO: 2020/11/15
// 注意:在 Kotlin 中使用時,需要将@Rule 更改為 @get:Rule
// 或者 使用 @Rule @JvmField
//@get:Rule
@Rule @JvmField
public val prompt = TestPormpt()
// @Before
// fun testStart(){
// println("測試開")
// }
//
// @After
// fun testStop(){
// println("測試關")
// }
@Test
fun testThat() {
assertThat("123", equalTo("123"))
}
}
複制
參考
- 廖雪峰-編寫JUnit測試
- Android單元測試(一):JUnit架構的使用