一.基本介紹
背景:
目前處于高速迭代開發中的Android項目往往需要除黑盒測試外更加可靠的質量保障,這正是單元測試的用武之地。單元測試周期性對項目進行函數級別的測試,在良好的覆蓋率下,能夠持續維護代碼邏輯,從而支持項目從容應對快速的版本更新。
正是由于測試在開發中的重要地位,才會在IT界刮起了 TDD 的旋風。TDD,也就是測試驅動開發模式。它旨在強調在開發功能代碼之前,先編寫測試代碼。也就是說在明確要開發某個功能后,首先思考如何對這個功能進行測試,并完成測試代碼的編寫,然后編寫相關的代碼滿足這些測試用例。然后循環進行添加其他功能,直到完成全部功能的開發。
二.Java 測試工具(框架)
1.JUnit(推薦使用JUnit4)
JUnit 在日常開發中還是很常用的,而且 Java 的各種 IDE (Eclipse、MyEclipse、IntelliJ IDEA)都集成了 JUnit 的組件。當然,自己添加插件也是很方便的。JUnit 框架是 Java 語言單元測試當前的一站式解決方案。這個框架值得稱贊,因為它把測試驅動的開發思想介紹給 Java 開發人員并教給他們如何有效地編寫單元測試。
2.TestNG
TestNG,即Testing Next Generation,下一代測試技術。是根據JUnit和NUnit思想,采用 jdk 的 annotation 技術來強化測試功能并借助XML 文件強化測試組織結構而構建的測試框架。TestNG 的強大之處還在于不僅可以用來做單元測試,還可以用來做集成測試。
重點介紹下JUnit4
JUnit是Java單元測試框架,已經在Eclipse中默認安裝。目前主流的有JUnit3和JUnit4。JUnit3中,測試用例需要繼承TestCase類。JUnit4中,測試用例無需繼承TestCase類,只需要使用@Test等注解,建議使用JUnit4。
JUnit4通過注解的方式來識別測試方法。目前支持的主要注解有:
- @BeforeClass 全局只會執行一次,而且是第一個運行
- @Before 在測試方法運行之前運行
- @Test 測試方法
- @After 在測試方法運行之后允許
- @AfterClass 全局只會執行一次,而且是最后一個運行
- @Ignore 忽略此方法
@Before 該方法在每次測試方法調用前都會調用 @Test 說明了該方法需要測試 @BeforeClass 該方法在所有測試方法之前調用,只會被調用一次 @After 該方法在每次測試方法調用后都會調用 @AfterClass 該方法在所有測試方法之后調用,只會被調用一次 @Ignore 忽略該方法
三.單元測試范圍
一般來說,單元測試任務包括
- 接口功能測試:用來保證接口功能的正確性。
- 局部數據結構測試(不常用):用來保證接口中的數據結構是正確的。 比如(1).變量有無初始值,(2).變量是否溢出.
-
邊界條件測試
(1).變量沒有賦值(即為NULL)
(2).變量是數值(或字符)
-主要邊界:最小值,最大值,無窮大(對于DOUBLE等)
-溢出邊界(期望異常或拒絕服務):最小值-1,最大值+1
-臨近邊界:最小值+1,最大值-1
(3). 變量是字符串
-引用“字符變量”的邊界
-空字符串
-對字符串長度應用“數值變量”的邊界
(4).變量是集合
-空集合
-對集合的大小應用“數值變量”的邊界
-調整次序:升序、降序
(5). 變量有規律
-比如對于Math.sqrt,給出n2-1,和n2+1的邊界
(6). 所有獨立執行通路測試:保證每一條代碼,每個分支都經過測試
-代碼覆蓋率
1>.語句覆蓋:保證每一個語句都執行到了
2>.判定覆蓋(分支覆蓋):保證每一個分支都執行到
3>.條件覆蓋:保證每一個條件都覆蓋到true和false(即if、while中的條件語句)
4>.路徑覆蓋:保證每一個路徑都覆蓋到
-相關軟件 (Cobertura:語句覆蓋)
- 各條錯誤處理通路測試:保證每一個異常都經過測試
如下是一個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("沒有實現");
}
// 使用“假設”來忽略測試方法
@Test
public void testAssume(){
System.out.println("Test assume");
// 當假設失敗時,則會停止運行,但這并不會意味測試方法失敗。
Assume.assumeTrue(false);
Assert.fail("沒有實現");
}
@After
public void tearDown() throws Exception {
System.out.println("Tear down");
}
@AfterClass
public static void tearDownAfterClass() {
System.out.println("Tear down After class");
}
}
運行結果:
四. 單元測試框架>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,他們的做法是通過實現一套JVM能運行的Android代碼,然后在unit test運行的時候去截取android相關的代碼調用,然后轉到他們的他們實現的代碼去執行這個調用的過程。
舉個例子說明一下,比如android里面有個類叫TextView
,他們實現了一個類叫ShadowTextView
。這個類基本上實現了TextView
的所有公共接口,假設你在unit test里面寫到
String text = textView.getText().toString();
。在這個unit test運行的時候,Robolectric
會自動判斷你調用了Android相關的代碼textView.getText()
,然后這個調用過程在底層截取了,轉到ShadowTextView
的getText
實現。而ShadowTextView
是真正實現了getText
這個方法的,所以這個過程便可以正常執行。
(2). 除了實現Android里面的類的現有接口,Robolectric還做了另外一件事情,極大地方便了unit testing的工作。那就是他們給每個Shadow類額外增加了很多接口,可以讀取對應的Android類的一些狀態。比如我們知道ImageView有一個方法叫setImageResource(resourceId),然而并沒有一個對應的getter方法叫getImageResourceId(),這樣你是沒有辦法測試這個ImageView是不是顯示了你想要的image。而在Robolectric實現的對應的ShadowImageView里面,則提供了getImageResourceId()這個接口。你可以用來測試它是不是正確的顯示了你想要的Image.環境配置
Android單元測試依舊需要JUnit框架的支持,Robolectric只是提供了Android代碼的運行環境。如果使用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的版本,要支持單元測試需要設置“Build Variants”,路徑是“View -->Tool Windows-->Build Variants”,然后設置為“Unit Tests”;當版本為2.0時,默認就支持。
圖2 單元測試工程位置
如圖1所示的綠色文件夾即是單元測試工程。這些代碼能夠檢測目標代碼的正確性,打包時單元測試的代碼不會被編譯進入APK中。
Robolectric最麻煩就是下載依賴! 由于我們生活在天朝,下載國外的依賴很慢,即使有了翻墻,效果也一般。
注意:第一次運行可能需要下載一些library,依賴庫,可能需要花一點時間,這個跟unit test本身沒關。
第二種方法:maven地址指向 阿里云的地址。
build.gradle
allprojects {
repositories {
//依賴庫,阿里云地址
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
jcenter()
}
}
具體原理參考: http://www.lxweimin.com/p/a01628c3ea16
五.Robolectric使用介紹
Mock
參考文章:
http://www.open-open.com/lib/view/open1470724287040.html
配置:
testCompile 'org.mockito:mockito-core:1.9.5'
說白了就是打樁(Stub)或則模擬,當你調用一個不好在測試中創建的對象時,Mock框架為你模擬一個和真實對象類似的替身來完成相應的行為。
mock
對象就是在調試期間用來作為真實對象的替代品。Mockito是Java中常見的Mock框架。
Robolectric在文檔中聲稱:“No Mocking Frameworks Required”:對于Robolectric的另一種可選方法是使用mock框架,比如Mockito;或者模擬出Android SDK。雖然這是個有效的方法,但基本上是應用代碼的反向實現。
Mockito雖然不能模擬final類、匿名類和Java基本類型;對于final方法和static方法,不能對其 when(…).thenReturn(…) 操作。另外mock對象,大多都需要植入到應用代碼中,從而進行verify(...)操作;但應用代碼中不一定有相應的set方法,如果要植入,就需要為了測試添加應用代碼。
但是, 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寫法介紹
對于一些依賴關系復雜的測試對象,可以采用Mock框架解除依賴,常用的有Mockito。例如Mock一個List類型的對象實例,可以采用如下方式:
List list = mock(List.class); //mock得到一個對象,也可以用@mock注入一個對象
所得到的list對象實例便是List類型的實例,如果不采用mock,List其實只是個接口,我們需要構造或者借助ArrayList才能進行實例化。與Shadow不同,Mock構造的是一個虛擬的對象,用于解耦真實對象所需要的依賴。Mock得到的對象僅僅是具備測試對象的類型,并不是真實的對象,也就是并沒有執行過真實對象的邏輯。
Mock也具備一些補充JUnit的驗證函數,比如設置函數的執行結果,示例如下:
When(sample.dosomething()).thenReturn(someAction);
//when(一個函數執行).thenReturn(一個可替代真實函數的結果的返回值);
//上述代碼是設置sample.dosomething()的返回值,當執行了sample.dosomething()這個函數時,
//就會得到someAction,從而解除了對真實的sample.dosomething()函數的依賴
上述代碼為被測函數定義一個可替代真實函數的結果的返回值。當使用這個函數后,這個可驗證的結果便會產生影響,從而代替函數的真實結果,這樣便解除了對真實函數的依賴。
同時Mock框架也可以驗證函數的執行次數,代碼如下:
List list = mock(List.class); //Mock得到一個對象
list.add(1); //執行一個函數
verify(list).add(1); //驗證這個函數的執行
verify(list,time(3)).add(1); //驗證這個函數的執行次數
在一些需要解除網絡依賴的場景中,多使用Mock。比如對retrofit框架的網絡依賴解除如下:
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";//這里是設置返回值
} 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可以由單元測試編寫者設置,而不來源于網絡,從而解除了對網絡環境的依賴。
Shadow
Robolectric的本質是在Java運行環境下,采用Shadow的方式對Android中的組件進行模擬測試,從而實現Android單元測試。對于一些Robolectirc暫不支持的組件,可以采用自定義Shadow的方式擴展Robolectric的功能。
Robolectric定義了大量的Shadow類,修改或者擴展了Android OS類的行為。當一個Android OS類被實例化,Robolectric會搜索相應的Shadow類;如果找到了,將創建與之關聯的Shadow對象。Android OS方法每次被調用時,Robolectirc確保:如果存在,Shadow類中的相應方法先被調用,這樣就有機會做測試相關邏輯。這種策略可運用于所有的方法,包括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的構造函數,Shadow還可以修改一些函數的功能,只需要在重載該函數的時候添加@Implementation,這種方式可以有效擴展Robolectric的功能。
Shadow是通過對真實的Android對象進行函數重載、初始化等方式對Android對象進行擴展,Shadow出來的對象的功能接近Android對象,可以看成是對Android對象一種修復。自定義的Shadow需要在config中聲明,聲明寫法是@Config(shadows=ShadowPoint.class)。
常見Robolectric用法
Robolectric支持單元測試范圍從Activity的跳轉、Activity展示View(包括菜單)和Fragment到View的點擊觸摸以及事件響應,同時Robolectric也能測試Toast和Dialog。對于需要網絡請求數據的測試,Robolectric可以模擬網絡請求的response。對于一些Robolectric不能測試的對象,比如ConcurrentTask,可以通過自定義Shadow的方式現實測試。下面將著重介紹Robolectric的常見用法。
- Activity展示測試與跳轉測試
創建網絡請求后,便可以測試Activity了。測試代碼如下:
@Test
public void testSampleActivity(){
SampleActivity sampleActivity=Robolectric.buildActivity(SampleActivity.class).
create().resume().get();
assertNotNull(sampleActivity);
assertEquals("Activity的標題", sampleActivity.getTitle());
}
Robolectric.buildActivity()用于構造Activity,create()函數執行后,該Activity會運行到onCreate周期,resume()則對應onResume周期。assertNotNull和assertEquals是JUnit中的斷言,Robolectric只提供運行環境,邏輯判斷還是需要依賴JUnit中的斷言。
Activity跳轉是Android開發的重要邏輯,其測試方法如下:
@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());
}
上述函數均需要在Dialog或Toast產生之后執行,能夠測試Dialog和Toast是否彈出。
Fragment展示與切換
Fragment是Activity的一部分,在Robolectric模擬執行Activity過程中,如果觸發了被測試的代碼中的Fragment添加邏輯,Fragment會被添加到Activity中。
需要注意Fragment出現的時機,如果目標Activity中的Fragment的添加是執行在onResume階段,在Activity被Robolectric執行resume()階段前,該Activity中并不會出現該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()函數的主體便是常用的添加fragment的代碼。切換一個Fragment往往由Activity中的代碼邏輯完成,需要Activity的引用。
控件的點擊以及可視驗證
@Test
public void testButtonClick(int buttonID){
Button submitButton = (Button) activity.findViewById(buttonID);
assertTrue(submitButton.isEnabled());
submitButton.performClick();
//驗證控件的行為
}
對控件的點擊驗證是調用performClick(),然后斷言驗證其行為。對于ListView這類涉及到Adapter的控件的點擊驗證,寫法如下:
//listView被展示之后
listView.performItemClick(listView.getAdapter().getView(position, null, null), 0, 0);
與button等控件稍有不同。
六.Robolectric單元測試編寫結構
如下實例:
未完待續......