天天看點

Junit | 不會寫單元測試,就如同不穿秋褲的熊孩子在冬天瞎跑

在我以往的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+回車),如圖所示

Junit | 不會寫單元測試,就如同不穿秋褲的熊孩子在冬天瞎跑

示例如下:

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

的作用就是解決這個問題。

Junit | 不會寫單元測試,就如同不穿秋褲的熊孩子在冬天瞎跑

常用的比對器整理:

比對器 說明 例子
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架構的使用