Android單元測試研究與實(shí)踐
Android單元測試介紹
處于高速迭代開發(fā)中的Android項(xiàng)目往往需要除黑盒測試外更加可靠的質(zhì)量保障,這正是單元測試的用武之地。單元測試周期性對(duì)項(xiàng)目進(jìn)行函數(shù)級(jí)別的測試,在良好的覆蓋率下,能夠持續(xù)維護(hù)代碼邏輯,從而支持項(xiàng)目從容應(yīng)對(duì)快速的版本更新。
單元測試是參與項(xiàng)目開發(fā)的工程師在項(xiàng)目代碼之外建立的白盒測試工程,用于執(zhí)行項(xiàng)目中的目標(biāo)函數(shù)并驗(yàn)證其狀態(tài)或者結(jié)果,其中,單元指的是測試的最小模塊,通常指函數(shù)。如圖1所示的綠色文件夾即是單元測試工程。這些代碼能夠檢測目標(biāo)代碼的正確性,打包時(shí)單元測試的代碼不會(huì)被編譯進(jìn)入APK中。
圖1 單元測試工程位置
與Java單元測試相同,Android單元測試也是維護(hù)代碼邏輯的白盒工程,但由于Android運(yùn)行環(huán)境的不同,Android單元測試的環(huán)境配置以及實(shí)施流程均有所不同。
Java單元測試
在傳統(tǒng)Java單元測試中,我們需要針對(duì)每個(gè)函數(shù)進(jìn)行設(shè)計(jì)單元測試用例。如圖2便是一個(gè)典型的單元測試的用例。
圖2 單元測試示例
上述示例中,針對(duì)函數(shù)dosomething(Boolean param)的每個(gè)分支,我們都需要構(gòu)造相應(yīng)的參數(shù)并驗(yàn)證結(jié)果。單元測試的目標(biāo)函數(shù)主要有三種:
- 有明確的返回值,如上圖的dosomething(Boolean param),做單元測試時(shí),只需調(diào)用這個(gè)函數(shù),然后驗(yàn)證函數(shù)的返回值是否符合預(yù)期結(jié)果。
- 這個(gè)函數(shù)只改變其對(duì)象內(nèi)部的一些屬性或者狀態(tài),函數(shù)本身沒有返回值,就驗(yàn)證它所改變的屬性和狀態(tài)。
- 一些函數(shù)沒有返回值,也沒有直接改變哪個(gè)值的狀態(tài),這就需要驗(yàn)證其行為,比如點(diǎn)擊事件。
既沒有返回值,也沒有改變狀態(tài),又沒有觸發(fā)行為的函數(shù)是不可測試的,在項(xiàng)目中不應(yīng)該存在。當(dāng)存在同時(shí)具備上述多種特性時(shí),本文建議采用多個(gè)case來真對(duì)每一種特性逐一驗(yàn)證,或者采用一個(gè)case,逐一執(zhí)行目標(biāo)函數(shù)并驗(yàn)證其影響。
構(gòu)造用例的原則是測試用例與函數(shù)一對(duì)一,實(shí)現(xiàn)條件覆蓋與路徑覆蓋。Java單元測試中,良好的單元測試是需要保證所有函數(shù)執(zhí)行正確的,即所有邊界條件都驗(yàn)證過,一個(gè)用例只測一個(gè)函數(shù),便于維護(hù)。在Android單元測試中,并不要求對(duì)所有函數(shù)都覆蓋到,像Android SDK中的函數(shù)回調(diào)則不用測試。
Android單元測試
在Android中,單元測試的本質(zhì)依舊是驗(yàn)證函數(shù)的功能,測試框架也是JUnit。在Java中,編寫代碼面對(duì)的只有類、對(duì)象、函數(shù),編寫單元測試時(shí)可以在測試工程中創(chuàng)建一個(gè)對(duì)象出來然后執(zhí)行其函數(shù)進(jìn)行測試,而在Android中,編寫代碼需要面對(duì)的是組件、控件、生命周期、異步任務(wù)、消息傳遞等,雖然本質(zhì)是SDK主動(dòng)執(zhí)行了一些實(shí)例的函數(shù),但創(chuàng)建一個(gè)Activity并不能讓它執(zhí)行到resume的狀態(tài),因此需要JUnit之外的框架支持。
當(dāng)前主流的單元測試框架AndroidTest和Robolectric,前者需要運(yùn)行在Android環(huán)境上,后者可以直接運(yùn)行在JVM上,速度也更快,可以直接由Jenkins周期性執(zhí)行,無需準(zhǔn)備Android環(huán)境。因此我們的單元測試基于Robolectric。對(duì)于一些測試對(duì)象依賴度較高而需要解除依賴的場景,我們可以借助Mock框架。
Android單元測試環(huán)境配置
Robolectric環(huán)境配置
Android單元測試依舊需要JUnit框架的支持,Robolectric只是提供了Android代碼的運(yùn)行環(huán)境。如果使用Robolectric 3.0,依賴配置如下:
- testCompile 'junit:junit:4.10'
- testCompile 'org.robolectric:robolectric:3.0'
Gradle對(duì)Robolectric 2.4的支持并不像3.0這樣好,但Robolectric 2.4所有的測試框架均在一個(gè)包里,另外參考資料也比較豐富,作者更習(xí)慣使用2.4。如果使用Robolectric 2.4,則需要如下配置:
- classpath 'org.robolectric:robolectric-gradle-plugin:0.14.+'//這行配置在buildscript的dependencies中
- apply plugin: 'robolectric'
- androidTestCompile 'org.robolectric:robolectric:2.4'
上述配置中,本文將testCompile寫成androidTest,并且常見的Android工程的單元測試目錄名稱有test也有androidTest,這兩種寫法并沒有功能上的差別,只是Android單元測試Test Artifact不同而已。Test Artifact如圖3所示:
圖3 Test Artifact
在Gradle插件中,這兩種Artifact執(zhí)行的Task還是有些區(qū)別的,但是并不影響單元測試的寫法與效果。雖然可以主動(dòng)配置單元測試的項(xiàng)目路徑,本文依舊建議采用與Test Artifact對(duì)應(yīng)的項(xiàng)目路徑和配置寫法。
Mock配置
如果要測試的目標(biāo)對(duì)象依賴關(guān)系較多,需要解除依賴關(guān)系,以免測試用例過于復(fù)雜,用Robolectric的Shadow是個(gè)辦法,但是推薦更加簡單的Mock框架,比如Mockito,該框架可以模擬出對(duì)象來,而且本身提供了一些驗(yàn)證函數(shù)執(zhí)行的功能。Mockito配置如下:
- repositories {
- jcenter()
- }
- dependencies {
- testCompile "org.mockito:mockito-core:1.+"
- }
Robolectric使用介紹
Robolectric單元測試編寫結(jié)構(gòu)
單元測試代碼寫在項(xiàng)目的test(也可能是androidTest,該目錄在項(xiàng)目中會(huì)呈淺綠色)目錄下。單元測試也是一個(gè)標(biāo)準(zhǔn)的Java工程,以類為文件單位編寫,執(zhí)行的最小單位是函數(shù),測試用例(以下簡稱case)是帶有@Test注解的函數(shù),單元測試?yán)锩鎺в衏ase的類由Robolectric框架執(zhí)行,需要為該類添加注解@RunWith(RobolectricTestRunner.class)?;赗obolectric的代碼結(jié)構(gòu)如下:
- //省略一堆import
- @RunWith(RobolectricTestRunner.class)
- public class MainActivityTest {
- @Before
- public void setUp() {
- //執(zhí)行初始化的操作
- }
- <a href="http://www.jobbole.com/members/madao">@Test</a>
- public void testCase() {
- //執(zhí)行各種測試邏輯判斷
- }
- }
上述結(jié)構(gòu)中,帶有@Before注解的函數(shù)在該類實(shí)例化后,會(huì)立即執(zhí)行,通常用于執(zhí)行一些初始化的操作,比如構(gòu)造網(wǎng)絡(luò)請(qǐng)求和構(gòu)造Activity。帶有@test注解的是單元測試的case,由Robolectric執(zhí)行,這些case本身也是函數(shù),可以在其他函數(shù)中調(diào)用,因此,case也是可以復(fù)用的。每個(gè)case都是獨(dú)立的,case不會(huì)互相影響,即便是相互調(diào)用也不會(huì)存在多線程干擾的問題。
常見Robolectric用法
Robolectric支持單元測試范圍從Activity的跳轉(zhuǎn)、Activity展示View(包括菜單)和Fragment到View的點(diǎn)擊觸摸以及事件響應(yīng),同時(shí)Robolectric也能測試Toast和Dialog。對(duì)于需要網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)的測試,Robolectric可以模擬網(wǎng)絡(luò)請(qǐng)求的response。對(duì)于一些Robolectric不能測試的對(duì)象,比如ConcurrentTask,可以通過自定義Shadow的方式現(xiàn)實(shí)測試。下面將著重介紹Robolectric的常見用法。
Robolectric 2.4模擬網(wǎng)絡(luò)請(qǐng)求
由于商業(yè)App的多數(shù)Activity界面數(shù)據(jù)都是通過網(wǎng)絡(luò)請(qǐng)求獲取,因?yàn)榫W(wǎng)絡(luò)請(qǐng)求是大多數(shù)App首要處理的模塊,測試依賴網(wǎng)絡(luò)數(shù)據(jù)的Activity時(shí),可以在@Before標(biāo)記的函數(shù)中準(zhǔn)備網(wǎng)絡(luò)數(shù)據(jù),進(jìn)行網(wǎng)絡(luò)請(qǐng)求的模擬。準(zhǔn)備網(wǎng)絡(luò)請(qǐng)求的代碼如下:
- public void prepareHttpResponse(String filePath) throws IOException {
- String netData = FileUtils.readFileToString(FileUtils.
- toFile(getClass().getResource(filePath)), HTTP.UTF_8);
- Robolectric.setDefaultHttpResponse(200, netData);
- }//代碼適用于Robolectric 2.4,3.0需要注意網(wǎng)絡(luò)請(qǐng)求的包的位置
由于Robolectric 2.4并不會(huì)發(fā)送網(wǎng)絡(luò)請(qǐng)求,因此需要本地創(chuàng)建網(wǎng)絡(luò)請(qǐng)求所返回的數(shù)據(jù),上述函數(shù)的filePath便是本地?cái)?shù)據(jù)的文件的路徑,setDefaultHttpResponse()則創(chuàng)建了該請(qǐng)求的Response。上述函數(shù)執(zhí)行后,單元測試工程便擁有了與本地?cái)?shù)據(jù)數(shù)據(jù)對(duì)應(yīng)的網(wǎng)絡(luò)請(qǐng)求,在這個(gè)函數(shù)執(zhí)行后展示的Activity便是有數(shù)據(jù)的Activity。
在Robolectric 3.0環(huán)境下,單元測試可以發(fā)真的請(qǐng)求,并且能夠請(qǐng)求到數(shù)據(jù),本文依舊建議采用mock的辦法構(gòu)造網(wǎng)絡(luò)請(qǐng)求,而不要依賴網(wǎng)絡(luò)環(huán)境。
Activity展示測試與跳轉(zhuǎn)測試
創(chuàng)建網(wǎng)絡(luò)請(qǐng)求后,便可以測試Activity了。測試代碼如下:
- <a href="http://www.jobbole.com/members/madao">@Test</a>
- public void testSampleActivity(){
- SampleActivity sampleActivity=Robolectric.buildActivity(SampleActivity.class).
- create().resume().get();
- assertNotNull(sampleActivity);
- assertEquals("Activity的標(biāo)題", sampleActivity.getTitle());
- }
Robolectric.buildActivity()用于構(gòu)造Activity,create()函數(shù)執(zhí)行后,該Activity會(huì)運(yùn)行到onCreate周期,resume()則對(duì)應(yīng)onResume周期。assertNotNull和assertEquals是JUnit中的斷言,Robolectric只提供運(yùn)行環(huán)境,邏輯判斷還是需要依賴JUnit中的斷言。
Activity跳轉(zhuǎn)是Android開發(fā)的重要邏輯,其測試方法如下:
- <a href="http://www.jobbole.com/members/madao">@Test</a>
- public void testActivityTurn(ActionBarActivity firstActivity, Class secondActivity) {
- Intent intent = new Intent(firstActivity.getApplicationContext(), secondActivity);
- assertEquals(intent, Robolectric.shadowOf(firstActivity).getNextStartedActivity());//3.0的API與2.4不同
- }
Fragment展示與切換
Fragment是Activity的一部分,在Robolectric模擬執(zhí)行Activity過程中,如果觸發(fā)了被測試的代碼中的Fragment添加邏輯,F(xiàn)ragment會(huì)被添加到Activity中。
需要注意Fragment出現(xiàn)的時(shí)機(jī),如果目標(biāo)Activity中的Fragment的添加是執(zhí)行在onResume階段,在Activity被Robolectric執(zhí)行resume()階段前,該Activity中并不會(huì)出現(xiàn)該Fragment。采用Robolectric主動(dòng)添加Fragment的方法如下:
- <a href="http://www.jobbole.com/members/madao">@Test</a>
- public void addfragment(Activity activity, int fragmentContent){
- FragmentTestUtil.startFragment(activity.getSupportFragmentManager().findFragmentById(fragmentContent));
- Fragment fragment = activity.getSupportFragmentManager().findFragmentById(fragmentContent);
- assertNotNull(fragment);
- }
startFragment()函數(shù)的主體便是常用的添加fragment的代碼。切換一個(gè)Fragment往往由Activity中的代碼邏輯完成,需要Activity的引用。
控件的點(diǎn)擊以及可視驗(yàn)證
- <a href="http://www.jobbole.com/members/madao">@Test</a>
- public void testButtonClick(int buttonID){
- Button submitButton = (Button) activity.findViewById(buttonID);
- assertTrue(submitButton.isEnabled());
- submitButton.performClick();
- //驗(yàn)證控件的行為
- }
對(duì)控件的點(diǎn)擊驗(yàn)證是調(diào)用performClick(),然后斷言驗(yàn)證其行為。對(duì)于ListView這類涉及到Adapter的控件的點(diǎn)擊驗(yàn)證,寫法如下:
- //listView被展示之后
- listView.performItemClick(listView.getAdapter().getView(position, null, null), 0, 0);
與button等控件稍有不同。
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是否彈出。
Shadow寫法介紹
Robolectric的本質(zhì)是在Java運(yùn)行環(huán)境下,采用Shadow的方式對(duì)Android中的組件進(jìn)行模擬測試,從而實(shí)現(xiàn)Android單元測試。對(duì)于一些Robolectirc暫不支持的組件,可以采用自定義Shadow的方式擴(kuò)展Robolectric的功能。
- @Implements(Point.class)
- public class ShadowPoint {
- @RealObject private Point realPoint;
- ...
- public void __constructor__(int x, int y) {
- realPoint.x = x;
- realPoint.y = y;
- }
- }//樣例來源于Robolectric官網(wǎng)
上述實(shí)例中,@Implements是聲明Shadow的對(duì)象,@RealObject是獲取一個(gè)Android 對(duì)象,constructor則是該Shadow的構(gòu)造函數(shù),Shadow還可以修改一些函數(shù)的功能,只需要在重載該函數(shù)的時(shí)候添加@Implementation,這種方式可以有效擴(kuò)展Robolectric的功能。
Shadow是通過對(duì)真實(shí)的Android對(duì)象進(jìn)行函數(shù)重載、初始化等方式對(duì)Android對(duì)象進(jìn)行擴(kuò)展,Shadow出來的對(duì)象的功能接近Android對(duì)象,可以看成是對(duì)Android對(duì)象一種修復(fù)。自定義的Shadow需要在config中聲明,聲明寫法是@Config(shadows=ShadowPoint.class)。
Mock寫法介紹
對(duì)于一些依賴關(guān)系復(fù)雜的測試對(duì)象,可以采用Mock框架解除依賴,常用的有Mockito。例如Mock一個(gè)List類型的對(duì)象實(shí)例,可以采用如下方式:
- List list = mock(List.class); //mock得到一個(gè)對(duì)象,也可以用@mock注入一個(gè)對(duì)象
所得到的list對(duì)象實(shí)例便是List類型的實(shí)例,如果不采用mock,List其實(shí)只是個(gè)接口,我們需要構(gòu)造或者借助ArrayList才能進(jìn)行實(shí)例化。與Shadow不同,Mock構(gòu)造的是一個(gè)虛擬的對(duì)象,用于解耦真實(shí)對(duì)象所需要的依賴。Mock得到的對(duì)象僅僅是具備測試對(duì)象的類型,并不是真實(shí)的對(duì)象,也就是并沒有執(zhí)行過真實(shí)對(duì)象的邏輯。
Mock也具備一些補(bǔ)充JUnit的驗(yàn)證函數(shù),比如設(shè)置函數(shù)的執(zhí)行結(jié)果,示例如下:
- When(sample.dosomething()).thenReturn(someAction);//when(一個(gè)函數(shù)執(zhí)行).thenReturn(一個(gè)可替代真實(shí)函數(shù)的結(jié)果的返回值);
- //上述代碼是設(shè)置sample.dosomething()的返回值,當(dāng)執(zhí)行了sample.dosomething()這個(gè)函數(shù)時(shí),就會(huì)得到someAction,從而解除了對(duì)真實(shí)的sample.dosomething()函數(shù)的依賴
上述代碼為被測函數(shù)定義一個(gè)可替代真實(shí)函數(shù)的結(jié)果的返回值。當(dāng)使用這個(gè)函數(shù)后,這個(gè)可驗(yàn)證的結(jié)果便會(huì)產(chǎn)生影響,從而代替函數(shù)的真實(shí)結(jié)果,這樣便解除了對(duì)真實(shí)函數(shù)的依賴。
同時(shí)Mock框架也可以驗(yàn)證函數(shù)的執(zhí)行次數(shù),代碼如下:
- List list = mock(List.class); //Mock得到一個(gè)對(duì)象
- list.add(1); //執(zhí)行一個(gè)函數(shù)
- verify(list).add(1); //驗(yàn)證這個(gè)函數(shù)的執(zhí)行
- verify(list,time(3)).add(1); //驗(yàn)證這個(gè)函數(shù)的執(zhí)行次數(shù)
在一些需要解除網(wǎng)絡(luò)依賴的場景中,多使用Mock。比如對(duì)retrofit框架的網(wǎng)絡(luò)依賴解除如下:
- //代碼參考了參考文獻(xiàn)[3]
- 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ò),從而解除了對(duì)網(wǎng)絡(luò)環(huán)境的依賴。
在實(shí)際項(xiàng)目中使用Robolectric構(gòu)建單元測試
單元測試的范圍
在Android項(xiàng)目中,單元測試的對(duì)象是組件狀態(tài)、控件行為、界面元素和自定義函數(shù)。本文并不推薦對(duì)每個(gè)函數(shù)進(jìn)行一對(duì)一的測試,像onStart()、onDestroy()這些周期函數(shù)并不需要全部覆蓋到。商業(yè)項(xiàng)目多采用Scrum模式,要求快速迭代,有時(shí)候未必有較多的時(shí)間寫單元測試,不再要求逐個(gè)函數(shù)寫單元測試。
本文單元測試的case多來源于一個(gè)簡短的業(yè)務(wù)邏輯,單元測試case需要對(duì)這段業(yè)務(wù)邏輯進(jìn)行驗(yàn)證。在驗(yàn)證的過程中,開發(fā)人員可以深度了解業(yè)務(wù)流程,同時(shí)新人來了看一下項(xiàng)目單元測試就知道哪個(gè)邏輯跑了多少函數(shù),需要注意哪些邊界——是的,單元測試需要像文檔一樣具備業(yè)務(wù)指導(dǎo)能力。
在大型項(xiàng)目中,遇到需要改動(dòng)基類中代碼的需求時(shí),往往不能準(zhǔn)確快速地知道改動(dòng)后的影響范圍,緊急時(shí)多采用創(chuàng)建子類覆蓋父類函數(shù)的辦法,但這不是長久之計(jì),在足夠覆蓋率的單元測試支持下,跑一下單元測試就知道某個(gè)函數(shù)改動(dòng)后的影響,可以放心地修改基類。
美團(tuán)的Android單元測試編寫流程如圖4所示。
圖4 美團(tuán)Android單元測試編寫流程
單元測試最終需要輸出文檔式的單元測試代碼,為線上代碼提供良好的代碼穩(wěn)定性保證。
單元測試的流程
實(shí)際項(xiàng)目中,單元測試對(duì)象與頁面是一對(duì)一的,并不建議跨頁面,這樣的單元測試藕合度太大,維護(hù)困難。單元測試需要找到頁面的入口,分析項(xiàng)目頁面中的元素、業(yè)務(wù)邏輯,這里的邏輯不僅僅包括界面元素的展示以及控件組件的行為,還包括代碼的處理邏輯。然后可以創(chuàng)建單元測試case列表(列表用于紀(jì)錄項(xiàng)目中單元測試的范圍,便于單元測試的管理以及新人了解業(yè)務(wù)流程),列表中記錄單元測試對(duì)象的頁面,對(duì)象中的case邏輯以及名稱等。工程師可以根據(jù)這個(gè)列表開始寫單元測試代碼。
單元測試是工程師代碼級(jí)別的質(zhì)量保證工程,上述流程并不能完全覆蓋重要的業(yè)務(wù)邏輯以及邊界條件,因此,需要寫完后,看覆蓋率,找出單元測試中沒有覆蓋到的函數(shù)分支條件等,然后繼續(xù)補(bǔ)充單元測試case列表,并在單元測試工程代碼中補(bǔ)上case。
直到規(guī)劃的頁面中所有邏輯的重要分支、邊界條件都被覆蓋,該項(xiàng)目的單元測試結(jié)束。單元測試流程如圖5所示。
圖5 單元測試執(zhí)行流程
上述分析頁面入口所得到結(jié)果便是@Before標(biāo)記的函數(shù)中的代碼,之后的循環(huán)便是所有的case(@Test標(biāo)記的函數(shù))。
單元測試項(xiàng)目實(shí)踐
為了系統(tǒng)的介紹單元測試的實(shí)施過程,本文創(chuàng)建了一個(gè)小型的demo項(xiàng)目作為測試對(duì)象。demo的功能是供用戶發(fā)布所見的新聞到服務(wù)端,并瀏覽所有已經(jīng)發(fā)表的新聞,是個(gè)典型的自媒體應(yīng)用。該demo的開發(fā)和測試涉及到TextView、EditView、ListView、Button以及自定義View,包含了網(wǎng)絡(luò)請(qǐng)求、多線程、異步任務(wù)以及界面跳轉(zhuǎn)等。能夠?yàn)槎鄶?shù)商業(yè)項(xiàng)目提供參照樣例。項(xiàng)目頁面如圖6所示。
圖6 單元測試case設(shè)計(jì)
首先需要分析App的每個(gè)頁面,針對(duì)頁面提取出簡短的業(yè)務(wù)邏輯,提取出的業(yè)務(wù)邏輯如圖6綠色圈圖所示。根據(jù)這些邏輯來設(shè)計(jì)單元測試的case(帶有@Test注解的那個(gè)函數(shù)),這里的業(yè)務(wù)邏輯不僅指需求中的業(yè)務(wù),還包括其他需要維護(hù)的代碼邏輯。業(yè)務(wù)流程不允許跨頁面,以免增加單元測試case的維護(hù)成本。針對(duì)demo中界面的單元測試case設(shè)計(jì)如下:
表1 單元測試case列表
接下來需要在單元測試工程中實(shí)現(xiàn)上述case,最小斷言數(shù)是業(yè)務(wù)邏輯上的判斷,并不是代碼的邊界條件,真實(shí)的case需要考慮代碼的邊界條件,比如數(shù)組為空等條件,因此,最終的斷言數(shù)量會(huì)大于等于最小斷言數(shù)。在需求業(yè)務(wù)上,最小斷言數(shù)也是該需求的業(yè)務(wù)條件。
寫完case后需要跑一遍單元測試并檢查覆蓋率報(bào)告,當(dāng)覆蓋率報(bào)告中缺少有些單元測試case列表中沒有但是實(shí)際邏輯中會(huì)有的邏輯時(shí),需要更新單元測試case列表,添加遺漏的邏輯,并將對(duì)應(yīng)的代碼補(bǔ)上。直到所有需要維護(hù)的邏輯都被覆蓋,該項(xiàng)目中的單元測試才算完成。單元測試并不是QA的黑盒測試,需要保證對(duì)代碼邏輯的覆蓋。
對(duì)表1分析,第一個(gè)頁面的“發(fā)布新聞”的case可以直接調(diào)用“編寫新聞”的case,以滿足條件“2.編寫了新聞的前提下,點(diǎn)擊發(fā)布按鈕”,在JUnit框架下,case(帶@Test注解的那個(gè)函數(shù))也是個(gè)函數(shù),直接調(diào)用這個(gè)函數(shù)就不是case,和case是無關(guān)的,兩者并不會(huì)相互影響,可以直接調(diào)用以減少重復(fù)代碼。第二個(gè)頁面不同于第一個(gè),一進(jìn)入就需要網(wǎng)絡(luò)請(qǐng)求,后續(xù)業(yè)務(wù)都需要依賴這個(gè)網(wǎng)絡(luò)請(qǐng)求,單元測試不應(yīng)該對(duì)某一個(gè)條件過度耦合,因此,需要用mock解除耦合,直接mock出網(wǎng)絡(luò)請(qǐng)求得到的數(shù)據(jù),單獨(dú)驗(yàn)證頁面對(duì)數(shù)據(jù)的響應(yīng)。
總結(jié)
單元測試并不是一個(gè)能直接產(chǎn)生回報(bào)的工程,它的運(yùn)行以及覆蓋率也不能直接提升代碼質(zhì)量,但其帶來的代碼控制力能夠大幅度降低大規(guī)模協(xié)同開發(fā)的風(fēng)險(xiǎn)?,F(xiàn)在的商業(yè)App開發(fā)都是大型團(tuán)隊(duì)協(xié)作開發(fā),不斷會(huì)有新人加入,無論新人是剛?cè)胄械膽?yīng)屆生還是工作多年,在代碼存在一定業(yè)務(wù)耦合度的時(shí)候,修改代碼就有一定風(fēng)險(xiǎn),可能會(huì)影響之前比較隱蔽的業(yè)務(wù)邏輯,或者是丟失曾經(jīng)的補(bǔ)丁,如果有高覆蓋率的單元測試工程,就能很快定位到新增代碼對(duì)現(xiàn)有項(xiàng)目的影響,與QA驗(yàn)收不同,這種影響是代碼級(jí)的。
在本文所設(shè)計(jì)的單元測試流程中,單元測試的case和具體頁面的具體業(yè)務(wù)流程以及該業(yè)務(wù)的代碼邏輯緊密聯(lián)系,單元測試如同技術(shù)文檔一般,能夠體現(xiàn)出一個(gè)業(yè)務(wù)邏輯運(yùn)行了多少函數(shù),需要注意什么樣的條件。這是一種新人了解業(yè)務(wù)流程、對(duì)業(yè)務(wù)進(jìn)行代碼級(jí)別融入的好辦法,看一下以前的單元測試case,就能知道與該case對(duì)應(yīng)的那個(gè)頁面上的那個(gè)業(yè)務(wù)邏輯會(huì)執(zhí)行多少函數(shù),以及這些函數(shù)可能出現(xiàn)的結(jié)果。
參考文獻(xiàn)
[1] http://robolectric.org
[2] https://github.com/square/okhttp/tree/master/mockwebserver
[3] http://stackoverflow.com/questions/17544751/square-retrofit-server-mock-for-testing
[4] https://en.wikipedia.org/wiki/Unit_testing