一.基本介紹
背景:
目前處于高速迭代開發(fā)中的Android項目往往需要除黑盒測試外更加可靠的質(zhì)量保障霹俺,這正是單元測試的用武之地敬特。單元測試周期性對項目進行函數(shù)級別的測試掰邢,在良好的覆蓋率下,能夠持續(xù)維護代碼邏輯伟阔,從而支持項目從容應(yīng)對快速的版本更新辣之。
正是由于測試在開發(fā)中的重要地位,才會在IT界刮起了 TDD 的旋風(fēng)皱炉。TDD怀估,也就是測試驅(qū)動開發(fā)模式。它旨在強調(diào)在開發(fā)功能代碼之前合搅,先編寫測試代碼奏夫。也就是說在明確要開發(fā)某個功能后,首先思考如何對這個功能進行測試历筝,并完成測試代碼的編寫,然后編寫相關(guān)的代碼滿足這些測試用例廊谓。然后循環(huán)進行添加其他功能梳猪,直到完成全部功能的開發(fā)。
二.Java 測試工具(框架)
1.JUnit(推薦使用JUnit4)
JUnit 在日常開發(fā)中還是很常用的蒸痹,而且 Java 的各種 IDE (Eclipse春弥、MyEclipse、IntelliJ IDEA)都集成了 JUnit 的組件叠荠。當然匿沛,自己添加插件也是很方便的。JUnit 框架是 Java 語言單元測試當前的一站式解決方案榛鼎。這個框架值得稱贊逃呼,因為它把測試驅(qū)動的開發(fā)思想介紹給 Java 開發(fā)人員并教給他們?nèi)绾斡行У鼐帉憜卧獪y試。
2.TestNG
TestNG者娱,即Testing Next Generation抡笼,下一代測試技術(shù)。是根據(jù)JUnit和NUnit思想黄鳍,采用 jdk 的 annotation 技術(shù)來強化測試功能并借助XML 文件強化測試組織結(jié)構(gòu)而構(gòu)建的測試框架推姻。TestNG 的強大之處還在于不僅可以用來做單元測試,還可以用來做集成測試框沟。
重點介紹下JUnit4
JUnit是Java單元測試框架藏古,已經(jīng)在Eclipse中默認安裝增炭。目前主流的有JUnit3和JUnit4。JUnit3中拧晕,測試用例需要繼承TestCase類隙姿。JUnit4中,測試用例無需繼承TestCase類防症,只需要使用@Test等注解,建議使用JUnit4孟辑。
JUnit4通過注解的方式來識別測試方法。目前支持的主要注解有:
- @BeforeClass 全局只會執(zhí)行一次蔫敲,而且是第一個運行
- @Before 在測試方法運行之前運行
- @Test 測試方法
- @After 在測試方法運行之后允許
- @AfterClass 全局只會執(zhí)行一次饲嗽,而且是最后一個運行
- @Ignore 忽略此方法
@Before 該方法在每次測試方法調(diào)用前都會調(diào)用 @Test 說明了該方法需要測試 @BeforeClass 該方法在所有測試方法之前調(diào)用,只會被調(diào)用一次 @After 該方法在每次測試方法調(diào)用后都會調(diào)用 @AfterClass 該方法在所有測試方法之后調(diào)用奈嘿,只會被調(diào)用一次 @Ignore 忽略該方法
三.單元測試范圍
一般來說貌虾,單元測試任務(wù)包括
- 接口功能測試:用來保證接口功能的正確性。
- 局部數(shù)據(jù)結(jié)構(gòu)測試(不常用):用來保證接口中的數(shù)據(jù)結(jié)構(gòu)是正確的裙犹。 比如(1).變量有無初始值,(2).變量是否溢出.
-
邊界條件測試
(1).變量沒有賦值(即為NULL)
(2).變量是數(shù)值(或字符)
-主要邊界:最小值尽狠,最大值,無窮大(對于DOUBLE等)
-溢出邊界(期望異骋镀裕或拒絕服務(wù)):最小值-1袄膏,最大值+1
-臨近邊界:最小值+1,最大值-1
(3). 變量是字符串
-引用“字符變量”的邊界
-空字符串
-對字符串長度應(yīng)用“數(shù)值變量”的邊界
(4).變量是集合
-空集合
-對集合的大小應(yīng)用“數(shù)值變量”的邊界
-調(diào)整次序:升序掺冠、降序
(5). 變量有規(guī)律
-比如對于Math.sqrt沉馆,給出n2-1,和n2+1的邊界
(6). 所有獨立執(zhí)行通路測試:保證每一條代碼德崭,每個分支都經(jīng)過測試
-代碼覆蓋率
1>.語句覆蓋:保證每一個語句都執(zhí)行到了
2>.判定覆蓋(分支覆蓋):保證每一個分支都執(zhí)行到
3>.條件覆蓋:保證每一個條件都覆蓋到true和false(即if斥黑、while中的條件語句)
4>.路徑覆蓋:保證每一個路徑都覆蓋到
-相關(guān)軟件 (Cobertura:語句覆蓋)
- 各條錯誤處理通路測試:保證每一個異常都經(jīng)過測試
如下是一個JUnit4的示例:
/**
* Created by huanming on 17/3/13.
*/
public class Junit4TestCase {
@BeforeClass
public static void setUpBeforeClass() {
System.out.println("Set up before class");
}
@Before
public void setUp() throws Exception {
System.out.println("Set up");
}
@Test
public void testMathPow() {
System.out.println("Test Math.pow");
Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0);
}
@Test
public void testMathMin() {
System.out.println("Test Math.min");
Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0);
}
// 期望此方法拋出NullPointerException異常
@Test(expected = NullPointerException.class)
public void testException() {
System.out.println("Test exception");
Object obj = null;
obj.toString();
}
// 忽略此測試方法
@Ignore
@Test
public void testMathMax() {
Assert.fail("沒有實現(xiàn)");
}
// 使用“假設(shè)”來忽略測試方法
@Test
public void testAssume(){
System.out.println("Test assume");
// 當假設(shè)失敗時,則會停止運行眉厨,但這并不會意味測試方法失敗锌奴。
Assume.assumeTrue(false);
Assert.fail("沒有實現(xiàn)");
}
@After
public void tearDown() throws Exception {
System.out.println("Tear down");
}
@AfterClass
public static void tearDownAfterClass() {
System.out.println("Tear down After class");
}
}
運行結(jié)果:
四. 單元測試框架>Robolectric
參考文章:
http://robolectric.org
https://github.com/robolectric/robolectric
https://en.wikipedia.org/wiki/Unit_testing
https://github.com/square/okhttp/tree/master/mockwebserver
介紹
(1). Robolectric 是一個開源的framework,他們的做法是通過實現(xiàn)一套JVM能運行的Android代碼憾股,然后在unit test運行的時候去截取android相關(guān)的代碼調(diào)用鹿蜀,然后轉(zhuǎn)到他們的他們實現(xiàn)的代碼去執(zhí)行這個調(diào)用的過程。
舉個例子說明一下服球,比如android里面有個類叫TextView
耻姥,他們實現(xiàn)了一個類叫ShadowTextView
。這個類基本上實現(xiàn)了TextView
的所有公共接口有咨,假設(shè)你在unit test里面寫到
String text = textView.getText().toString();
琐簇。在這個unit test運行的時候,Robolectric
會自動判斷你調(diào)用了Android相關(guān)的代碼textView.getText()
,然后這個調(diào)用過程在底層截取了婉商,轉(zhuǎn)到ShadowTextView
的getText
實現(xiàn)似忧。而ShadowTextView
是真正實現(xiàn)了getText
這個方法的,所以這個過程便可以正常執(zhí)行丈秩。
(2). 除了實現(xiàn)Android里面的類的現(xiàn)有接口盯捌,Robolectric還做了另外一件事情,極大地方便了unit testing的工作蘑秽。那就是他們給每個Shadow類額外增加了很多接口饺著,可以讀取對應(yīng)的Android類的一些狀態(tài)。比如我們知道ImageView有一個方法叫setImageResource(resourceId)肠牲,然而并沒有一個對應(yīng)的getter方法叫g(shù)etImageResourceId()幼衰,這樣你是沒有辦法測試這個ImageView是不是顯示了你想要的image。而在Robolectric實現(xiàn)的對應(yīng)的ShadowImageView里面缀雳,則提供了getImageResourceId()這個接口渡嚣。你可以用來測試它是不是正確的顯示了你想要的Image.環(huán)境配置
Android單元測試依舊需要JUnit框架的支持,Robolectric只是提供了Android代碼的運行環(huán)境肥印。如果使用Robolectric 3.0识椰,依賴配置如下:
testCompile 'junit:junit:4.12'
testCompile('org.robolectric:robolectric:3.0') {
exclude module: 'commons-logging'
}
Gradle對Robolectric 2.4的支持并不像3.0這樣好,但Robolectric 2.4所有的測試框架均在一個包里深碱,如果使用Robolectric 2.4腹鹉,則需要如下配置:
//這行配置在buildscript的dependencies中
classpath 'org.robolectric:robolectric-gradle-plugin:0.14.+'
apply plugin: 'robolectric'
androidTestCompile 'org.robolectric:robolectric:2.4'
需要注意:Android Studio小于2.0的版本,要支持單元測試需要設(shè)置“Build Variants”敷硅,路徑是“View -->Tool Windows-->Build Variants”种蘸,然后設(shè)置為“Unit Tests”;當版本為2.0時竞膳,默認就支持。
圖2 單元測試工程位置
如圖1所示的綠色文件夾即是單元測試工程诫硕。這些代碼能夠檢測目標代碼的正確性坦辟,打包時單元測試的代碼不會被編譯進入APK中。
Robolectric最麻煩就是下載依賴章办! 由于我們生活在天朝锉走,下載國外的依賴很慢,即使有了翻墻藕届,效果也一般挪蹭。
注意:第一次運行可能需要下載一些library,依賴庫休偶,可能需要花一點時間梁厉,這個跟unit test本身沒關(guān)。
第二種方法:maven地址指向 阿里云的地址。
build.gradle
allprojects {
repositories {
//依賴庫词顾,阿里云地址
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
jcenter()
}
}
具體原理參考: http://www.reibang.com/p/a01628c3ea16
五.Robolectric使用介紹
Mock
參考文章:
http://www.open-open.com/lib/view/open1470724287040.html
配置:
testCompile 'org.mockito:mockito-core:1.9.5'
說白了就是打樁(Stub)或則模擬八秃,當你調(diào)用一個不好在測試中創(chuàng)建的對象時,Mock框架為你模擬一個和真實對象類似的替身來完成相應(yīng)的行為肉盹。
mock
對象就是在調(diào)試期間用來作為真實對象的替代品昔驱。Mockito是Java中常見的Mock框架。
Robolectric在文檔中聲稱:“No Mocking Frameworks Required”:對于Robolectric的另一種可選方法是使用mock框架上忍,比如Mockito骤肛;或者模擬出Android SDK。雖然這是個有效的方法窍蓝,但基本上是應(yīng)用代碼的反向?qū)崿F(xiàn)腋颠。
Mockito雖然不能模擬final類、匿名類和Java基本類型它抱;對于final方法和static方法秕豫,不能對其 when(…).thenReturn(…) 操作。另外mock對象观蓄,大多都需要植入到應(yīng)用代碼中混移,從而進行verify(...)操作;但應(yīng)用代碼中不一定有相應(yīng)的set方法侮穿,如果要植入歌径,就需要為了測試添加應(yīng)用代碼。
但是亲茅, Mockito + Powermock可以解決上述的問題回铛。
示例:
@Implements(HttpClient.class)
public class ShadowHttpClient {
protected static boolean isHandleError = false;
protected static boolean isRaiseException = false;
public static String lastRequestPath;
public static String lastRequestData;
public static List<String> allExecutedAction = new ArrayList<String>();
public static List<String> allRequestData = new ArrayList<String>();
private static ResponseObjectConvert converter;
private static List<HttpResponseResult> responseResultList;
private static int position = 0;
@RealObject
HttpClient httpClient;
public void __constructor__(String host, int port, boolean isEncryptionEnabled) {
}
@Implementation
public HttpResponseResult sendRequestGetResponse(String path, String request) {
lastRequestPath = path;
lastRequestData = request;
allExecutedAction.add(path);
allRequestData.add(request);
if (isRaiseException) {
throw new RuntimeException();
}
if (converter != null) {
if (isHandleError) {
setResponseResultList(asList(new HttpResponseResult(FAILED, converter.convertResponse(), null)));
} else {
setResponseResultList(asList(new HttpResponseResult(SUCCEEDED, converter.convertResponse(), null)));
}
}
return responseResultList.get(position++);
}
@Implementation
public HttpResponseResult getResponse(String path) {
return sendRequestGetResponse(path,"");
}
public static void reset() {
lastRequestPath = null;
lastRequestData = null;
allExecutedAction.clear();
allRequestData.clear();
ShadowHttpClient.converter = null;
ShadowHttpClient.responseResultList = null;
ShadowHttpClient.isHandleError = false;
ShadowHttpClient.isRaiseException = false;
}
public static void setRaiseException(boolean isRaiseException) {
ShadowHttpClient.isRaiseException = isRaiseException;
}
public static void setConverter(ResponseObjectConvert converter) {
ShadowHttpClient.converter = converter;
}
public static void setHandleError(boolean handleError) {
ShadowHttpClient.isHandleError = handleError;
}
public static void setResponseResultList(List<HttpResponseResult> responseResultList) {
position = 0;
ShadowHttpClient.responseResultList = responseResultList;
}
public interface ResponseObjectConvert {
public String convertResponse();
}
Mock寫法介紹
對于一些依賴關(guān)系復(fù)雜的測試對象,可以采用Mock框架解除依賴克锣,常用的有Mockito茵肃。例如Mock一個List類型的對象實例,可以采用如下方式:
List list = mock(List.class); //mock得到一個對象袭祟,也可以用@mock注入一個對象
所得到的list對象實例便是List類型的實例验残,如果不采用mock,List其實只是個接口巾乳,我們需要構(gòu)造或者借助ArrayList才能進行實例化您没。與Shadow不同,Mock構(gòu)造的是一個虛擬的對象胆绊,用于解耦真實對象所需要的依賴氨鹏。Mock得到的對象僅僅是具備測試對象的類型,并不是真實的對象压状,也就是并沒有執(zhí)行過真實對象的邏輯仆抵。
Mock也具備一些補充JUnit的驗證函數(shù),比如設(shè)置函數(shù)的執(zhí)行結(jié)果,示例如下:
When(sample.dosomething()).thenReturn(someAction);
//when(一個函數(shù)執(zhí)行).thenReturn(一個可替代真實函數(shù)的結(jié)果的返回值);
//上述代碼是設(shè)置sample.dosomething()的返回值肢础,當執(zhí)行了sample.dosomething()這個函數(shù)時还栓,
//就會得到someAction,從而解除了對真實的sample.dosomething()函數(shù)的依賴
上述代碼為被測函數(shù)定義一個可替代真實函數(shù)的結(jié)果的返回值传轰。當使用這個函數(shù)后剩盒,這個可驗證的結(jié)果便會產(chǎn)生影響,從而代替函數(shù)的真實結(jié)果慨蛙,這樣便解除了對真實函數(shù)的依賴辽聊。
同時Mock框架也可以驗證函數(shù)的執(zhí)行次數(shù),代碼如下:
List list = mock(List.class); //Mock得到一個對象
list.add(1); //執(zhí)行一個函數(shù)
verify(list).add(1); //驗證這個函數(shù)的執(zhí)行
verify(list,time(3)).add(1); //驗證這個函數(shù)的執(zhí)行次數(shù)
在一些需要解除網(wǎng)絡(luò)依賴的場景中期贫,多使用Mock跟匆。比如對retrofit框架的網(wǎng)絡(luò)依賴解除如下:
public class MockClient implements Client {
@Override
public Response execute(Request request) throws IOException {
Uri uri = Uri.parse(request.getUrl());
String responseString = "";
if(uri.getPath().equals("/path/of/interest")) {
responseString = "返回的json1";//這里是設(shè)置返回值
} else {
responseString = "返回的json2";
}
return new Response(request.getUrl(), 200, "nothing", Collections.EMPTY_LIST, new TypedByteArray("application/json", responseString.getBytes()));
}
}
//MockClient使用方式如下:
RestAdapter.Builder builder = new RestAdapter.Builder();
builder.setClient(new MockClient());
這種方式下retrofit的response可以由單元測試編寫者設(shè)置,而不來源于網(wǎng)絡(luò)通砍,從而解除了對網(wǎng)絡(luò)環(huán)境的依賴玛臂。
Shadow
Robolectric的本質(zhì)是在Java運行環(huán)境下,采用Shadow的方式對Android中的組件進行模擬測試封孙,從而實現(xiàn)Android單元測試迹冤。對于一些Robolectirc暫不支持的組件,可以采用自定義Shadow的方式擴展Robolectric的功能虎忌。
Robolectric定義了大量的Shadow類泡徙,修改或者擴展了Android OS類的行為。當一個Android OS類被實例化膜蠢,Robolectric會搜索相應(yīng)的Shadow類堪藐;如果找到了,將創(chuàng)建與之關(guān)聯(lián)的Shadow對象挑围。Android OS方法每次被調(diào)用時礁竞,Robolectirc確保:如果存在,Shadow類中的相應(yīng)方法先被調(diào)用杉辙,這樣就有機會做測試相關(guān)邏輯模捂。這種策略可運用于所有的方法,包括static和final方法奏瞬。
@Implements(Point.class)
public class ShadowPoint {
@RealObject private Point realPoint;
...
public void __constructor__(int x, int y) {
realPoint.x = x;
realPoint.y = y;
}
}
上述實例中,@Implements是聲明Shadow的對象泉孩,@RealObject是獲取一個Android 對象硼端,constructor則是該Shadow的構(gòu)造函數(shù),Shadow還可以修改一些函數(shù)的功能寓搬,只需要在重載該函數(shù)的時候添加@Implementation珍昨,這種方式可以有效擴展Robolectric的功能。
Shadow是通過對真實的Android對象進行函數(shù)重載、初始化等方式對Android對象進行擴展镣典,Shadow出來的對象的功能接近Android對象兔毙,可以看成是對Android對象一種修復(fù)。自定義的Shadow需要在config中聲明兄春,聲明寫法是@Config(shadows=ShadowPoint.class)澎剥。
常見Robolectric用法
Robolectric支持單元測試范圍從Activity的跳轉(zhuǎn)、Activity展示View(包括菜單)和Fragment到View的點擊觸摸以及事件響應(yīng)赶舆,同時Robolectric也能測試Toast和Dialog哑姚。對于需要網(wǎng)絡(luò)請求數(shù)據(jù)的測試,Robolectric可以模擬網(wǎng)絡(luò)請求的response芜茵。對于一些Robolectric不能測試的對象叙量,比如ConcurrentTask,可以通過自定義Shadow的方式現(xiàn)實測試九串。下面將著重介紹Robolectric的常見用法绞佩。
- Activity展示測試與跳轉(zhuǎn)測試
創(chuàng)建網(wǎng)絡(luò)請求后,便可以測試Activity了猪钮。測試代碼如下:
@Test
public void testSampleActivity(){
SampleActivity sampleActivity=Robolectric.buildActivity(SampleActivity.class).
create().resume().get();
assertNotNull(sampleActivity);
assertEquals("Activity的標題", sampleActivity.getTitle());
}
Robolectric.buildActivity()用于構(gòu)造Activity品山,create()函數(shù)執(zhí)行后,該Activity會運行到onCreate周期躬贡,resume()則對應(yīng)onResume周期谆奥。assertNotNull和assertEquals是JUnit中的斷言,Robolectric只提供運行環(huán)境拂玻,邏輯判斷還是需要依賴JUnit中的斷言酸些。
Activity跳轉(zhuǎn)是Android開發(fā)的重要邏輯,其測試方法如下:
@Test
public void testMainActivity() {
MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
mainActivity.findViewById(R.id.textView1).performClick();
Intent expectedIntent = new Intent(mainActivity, SecondActivity.class);
ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);
Intent actualIntent = shadowActivity.getNextStartedActivity();
Assert.assertEquals(expectedIntent, actualIntent);
}
- Dialog和Toast測試
測試Dialog和Toast的方法如下:
public void testDialog(){
Dialog dialog = ShadowDialog.getLatestDialog();
assertNotNull(dialog);
}
public void testToast(String toastContent){
ShadowHandler.idleMainLooper();
assertEquals(toastContent, ShadowToast.getTextOfLatestToast());
}
上述函數(shù)均需要在Dialog或Toast產(chǎn)生之后執(zhí)行檐蚜,能夠測試Dialog和Toast是否彈出魄懂。
Fragment展示與切換
Fragment是Activity的一部分,在Robolectric模擬執(zhí)行Activity過程中闯第,如果觸發(fā)了被測試的代碼中的Fragment添加邏輯市栗,F(xiàn)ragment會被添加到Activity中。
需要注意Fragment出現(xiàn)的時機咳短,如果目標Activity中的Fragment的添加是執(zhí)行在onResume階段填帽,在Activity被Robolectric執(zhí)行resume()階段前,該Activity中并不會出現(xiàn)該Fragment咙好。采用Robolectric主動添加Fragment的方法如下:
@Test
public void addfragment(Activity activity, int fragmentContent){
FragmentTestUtil.startFragment(activity.getSupportFragmentManager().findFragmentById(fragmentContent));
Fragment fragment = activity.getSupportFragmentManager().findFragmentById(fragmentContent);
assertNotNull(fragment);
}
startFragment()函數(shù)的主體便是常用的添加fragment的代碼篡腌。切換一個Fragment往往由Activity中的代碼邏輯完成,需要Activity的引用勾效。
控件的點擊以及可視驗證
@Test
public void testButtonClick(int buttonID){
Button submitButton = (Button) activity.findViewById(buttonID);
assertTrue(submitButton.isEnabled());
submitButton.performClick();
//驗證控件的行為
}
對控件的點擊驗證是調(diào)用performClick()嘹悼,然后斷言驗證其行為叛甫。對于ListView這類涉及到Adapter的控件的點擊驗證,寫法如下:
//listView被展示之后
listView.performItemClick(listView.getAdapter().getView(position, null, null), 0, 0);
與button等控件稍有不同杨伙。
六.Robolectric單元測試編寫結(jié)構(gòu)
如下實例:
未完待續(xù)......