1.前言
網上有許多關於單元測試的好處,這裏我就不去說了。我寫單元測試的理由很簡單粗暴,就是圖一個方便。試想一下這個場景:我們在寫一個新功能,每寫一部分,我們就安裝到手機上查看一下,這個過程中你要點擊到對應的頁面,做對應的操作,最後才能反饋給你結果。如果達到了預期效果,那麼恭喜你。可是一旦這次失敗了,是不是又要重複這一過程?是不是感到很麻煩?很費時間?如果你想早點寫完下班,那麼你就需要掌握單元測試。因爲它能大大的縮短你自我驗證的時間。
2.準備工作
我們新建一個項目,模板代碼會默認在build文件中添加JUnit的依賴,而單元測試代碼是放在src/test/java下面的,如下圖:
用鼠標右鍵點擊測試方法,選擇菜單中的“Run”選項就可以執行對應的單元測試。我執行了圖中的測試代碼,可以看到執行方法只用了6毫秒,整個過程不到2秒。
3.JUnit介紹
JUnit是Java最基礎的測試框架,主要的作用就是斷言。
使用時在app的build文件中添加依賴。注意:用於測試環境框架一律是testCompile
開頭。
dependencies {
testCompile 'junit:junit:4.12'
}
Assert類中主要方法如下:
方法名 | 方法描述 |
---|---|
assertEquals | 斷言傳入的預期值與實際值是相等的 |
assertNotEquals | 斷言傳入的預期值與實際值是不相等的 |
assertArrayEquals | 斷言傳入的預期數組與實際數組是相等的 |
assertNull | 斷言傳入的對象是爲空 |
assertNotNull | 斷言傳入的對象是不爲空 |
assertTrue | 斷言條件爲真 |
assertFalse | 斷言條件爲假 |
assertSame | 斷言兩個對象引用同一個對象,相當於“==” |
assertNotSame | 斷言兩個對象引用不同的對象,相當於“!=” |
assertThat | 斷言實際值是否滿足指定的條件 |
注意:上面的每一個方法,都有對應的重載方法,可以在前面加一個String類型的參數,表示如果斷言失敗時的提示。
JUnit 中的常用註解:
註解名 | 含義 |
---|---|
@Test | 表示此方法爲測試方法 |
@Before | 在每個測試方法前執行,可做初始化操作 |
@After | 在每個測試方法後執行,可做釋放資源操作 |
@Ignore | 忽略的測試方法 |
@BeforeClass | 在類中所有方法前運行。此註解修飾的方法必須是static void |
@AfterClass | 在類中最後運行。此註解修飾的方法必須是static void |
@RunWith | 指定該測試類使用某個運行器 |
@Parameters | 指定測試類的測試數據集合 |
@Rule | 重新制定測試類中方法的行爲 |
@FixMethodOrder | 指定測試類中方法的執行順序 |
執行順序:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass
4.JUnit用法
我們測試下面這個簡單的時間轉換工具類,來說明一下具體的用法。
public class DateUtil {
/**
* 英文全稱 如:2017-11-01 22:11:00
*/
public static String FORMAT_YMDHMS = "yyyy-MM-dd HH:mm:ss";
/**
* 掉此方法輸入所要轉換的時間輸入例如("2017-11-01 22:11:00")返回時間戳
*
* @param time
* @return 時間戳
*/
public static long dateToStamp(String time) throws ParseException{
SimpleDateFormat sdr = new SimpleDateFormat(FORMAT_YMDHMS, Locale.CHINA);
Date date = sdr.parse(time);
return date.getTime();
}
/**
* 將時間戳轉換爲時間
*/
public static String stampToDate(long lt){
String res;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(FORMAT_YMDHMS, Locale.CHINA);
Date date = new Date(lt);
res = simpleDateFormat.format(date);
return res;
}
}
1.基礎用法
1.首先測試stampToDate
方法,測試時我認爲返回的結果等於“預期時間”這個字符串。測試方法執行後如下圖:
可以看到預期值與實際結果不符,測試失敗!想要測試成功要麼預期值爲”2017-10-15 16:00:02”要麼使用assertNotEquals
方法斷言。
2.接下來測試dateToStamp
方法。
很簡單,我認爲返回結果不等於4,結果測試通過。
3.我們注意到在dateToStamp
方法中,有拋出一個解析異常(ParseException)。也就是當參數沒有按照規定格式去傳,就會導致這個異常。
那我們怎麼驗證一個方法是否拋出了異常呢?可以給@Test
註解設置expected
參數來實現,如下:
拋出了對應的異常則測試成功,反之則測試失敗。
2.參數化測試
這時,你是不是覺得還是很麻煩,因爲每次測試一個方法都要去設置對應的值,就不能連續用不不同的值去測試一個方法,省的我們不斷地修改。這時就用到了@RunWith
與@Parameters
。
首先在測試類上添加註解@RunWith(Parameterized.class)
,在創建一個由 @Parameters
註解的public static方法,讓返回一個對應的測試數據集合。最後創建構造方法,方法的參數順序和類型與測試數據集合一一對應。
上圖就是一個簡單的例子,可以看到連續執行了三次測試,其中第二次測試沒有拋出異常,測試失敗!
3.assertThat用法
上面我們所用到的一些基本的斷言,如果我們沒有設置失敗時的輸出信息,那麼在斷言失敗時只會拋出AssertionError
,無法知道到底是哪一部分出錯。而assertThat
就幫我們解決了這一點。它的可讀性更好。
assertThat(T actual, Matcher<? super T> matcher);
assertThat(String reason, T actual, Matcher<? super T> matcher);
其中reason
爲斷言失敗時的輸出信息,actual
爲斷言的值,matcher
爲斷言的匹配器。
常用的匹配器整理:
匹配器 | 說明 | 例子 |
---|---|---|
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)); |
下圖爲使用assertThat
測試失敗時所顯示的具體錯誤信息。可以看到錯誤信息很詳細!
當然了匹配器也是可以自定義的。這裏我自定義一個字符串是否是手機號碼的匹配器來演示一下。
只需要繼承BaseMatcher
抽象類,實現matches
與describeTo
方法。代碼如下:
public class IsMobilePhoneMatcher extends BaseMatcher<String> {
/**
* 進行斷言判定,返回true則斷言成功,否則斷言失敗
*/
@Override
public boolean matches(Object item) {
if (item == null) {
return false;
}
Pattern pattern = Pattern.compile("(1|861)(3|5|7|8)\\d{9}$*");
Matcher matcher = pattern.matcher((String) item);
return matcher.find();
}
/**
* 給期待斷言成功的對象增加描述
*/
@Override
public void describeTo(Description description) {
description.appendText("預計此字符串是手機號碼!");
}
/**
* 給斷言失敗的對象增加描述
*/
@Override
public void describeMismatch(Object item, Description description) {
description.appendText(item.toString() + "不是手機號碼!");
}
}
執行單元測試如下:
正確的手機號碼測試成功:
錯誤號碼測試失敗:
5.@Rule用法
還記得一開始我們在@Before
與@After
註解的方法中加入”測試開始”的提示信息嗎?假如我們一直需要這樣的提示,那是不是需要每次在測試類中去實現它。這樣就會比較麻煩。這時你就可以使用@Rule
來解決這個問題,它甚至比@Before
與@After
還要強大。
自定義@Rule
很簡單,就是實現TestRule
接口,實現apply
方法。代碼如下:
public class MyRule implements TestRule {
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
// evaluate前執行方法相當於@Before
String methodName = description.getMethodName(); // 獲取測試方法的名字
System.out.println(methodName + "測試開始!");
base.evaluate(); // 運行的測試方法
// evaluate後執行方法相當於@After
System.out.println(methodName + "測試結束!");
}
};
}
}
我們使用一下我們自定義的MyRule
,效果如圖:
5.參考
PS:計劃開始寫有關Android單元測試的內容,因爲涉及的測試框架比較多,所以由簡至難開始,最終達到日常開發實用的階段。(沒想到這篇前後就用了一整天。。。)我也儘量快速的更新這一系列。代碼已上傳至Github。希望大家多多點贊支持!