這篇文章為《解讀Android官方MVP項目單元測試》(以下簡稱《解讀》)的附錄部分,行此文的目的有二,其一是這個項目的單元測試齊全,覆蓋率很高,有極高的學(xué)習(xí)價值,筆者希望把每個測試用例都描述一遍,通過這種方式來強迫自己認(rèn)真的看完。其二,這部分內(nèi)容難免枯燥,筆者盡力想把它寫得可讀性高一點點,卻發(fā)現(xiàn)著實有難度,簡直是給自己挖坑,所以從《寫點有價值的測試用例》的角度出發(fā),對這篇附錄文章稍作修飾。但不管這樣,這個MVP項目及其單元測試用例對我們的工作可以帶來很多思路,所以不妨速讀一遍。
一 什么是有價值的測試用例
以這個項目為例,我覺得對于測試用例的設(shè)計,不能離開架構(gòu)層面和業(yè)務(wù)層面。
- 架構(gòu)層面
不同的架構(gòu),決定著測試用例的寫法不一,比如MVC或者M(jìn)VP,每一層負(fù)責(zé)的測試職責(zé)是不一樣的。 以todo-mvp這個項目為例,筆者在《解讀》中已經(jīng)提到,一個功能的測試需要MVP三層的協(xié)作,彼此各司其職,卻又互相聯(lián)系,這里再做一番總結(jié):
- Presenter層:這一層很清晰,我們?yōu)樗拿總€接口方法,以及每個方法里涉及的多個邏輯路徑設(shè)計相應(yīng)的測試用例,值得注意的是,這一層我們較少做輸入輸出的斷言,而是驗證是否正確覆蓋V層和M層的邏輯。
- Model層:同上,我們?yōu)樗拿總€方法設(shè)計測試用例,與P層不同,這一層要斷言輸入輸出數(shù)據(jù)是否準(zhǔn)確。
- View層:這一層我們放在業(yè)務(wù)層面來講。
- 業(yè)務(wù)層面
做單元測試,測試業(yè)務(wù)邏輯是重中之重。View層承擔(dān)著這一重任,設(shè)計這一層的測試用例時,不要想太多,站在我們正常使用功能的角度出發(fā),把交互行為翻譯成Espresso的代碼。由于View是入口,當(dāng)一種交互行為發(fā)生,Presenter開始調(diào)度View和Model層各自執(zhí)行邏輯,因此從這個角度來講,View層的測試涵蓋了MVP三層的邏輯。
聊完有價值的,我們再來看看什么是沒價值的測試用例。比如以下幾種:
- 對成熟的工具類進(jìn)行測試
- 對簡單的方法進(jìn)行測試(比如get、set方法)
- MVP各層重復(fù)測試,比如P層去斷言輸入輸出的正確性
接下來筆者將完整的展示這個MVP項目中的所有單元測試用例,分為三個維度,androidTest下的、androidTestMock下的和test下的所有測試用例,如果覺得閱讀起來枯燥,可以直接閱讀每個測試類開篇的概述部分。
二 androidTest文件下的測試
V層:導(dǎo)航界面測試——AppNavigationTest
概述:該測試用例做導(dǎo)航測試,即對DrawerLayout打開、關(guān)閉、點擊Item后打開的Activity等功能進(jìn)行測試。
意義:告訴我們?nèi)绾螌rawerLayout設(shè)計有價值的測試用例。
-
clickOnStatisticsNavigationItem_ShowsStatisticsScreen
打開Left Drawer->點擊Statistics按鈕->斷言StatisticsActivity已經(jīng)打開 -
clickOnListNavigationItem_ShowsListScreen
打開Left Drawer->點擊Statistics按鈕->打開Left Drawer->點擊TO-DO list按鈕->斷言TasksActivity已經(jīng)打開 -
clickOnAndroidHomeIcon_OpensNavigation
驗證通過ActionBar的icon進(jìn)行關(guān)閉和打開Left Drawer
V層:任務(wù)模塊界面測試——TasksScreenTest
概述:該測試用例針對任務(wù)列表和任務(wù)詳情頁的界面功能測試,涵蓋所有頁面上的交互,包括增刪改查任務(wù)、改變?nèi)蝿?wù)狀態(tài),過濾任務(wù)列表等,除此之外還驗證了橫豎屏的交互對界面數(shù)據(jù)狀態(tài)的影響。
意義:告訴我們?nèi)绾卧O(shè)計有價值的功能界面測試用例。
-
clickAddTaskButton_opensAddTaskUi
點擊添加按鈕->斷言相應(yīng)Activity已經(jīng)打開 -
addTaskToTasksList
添加標(biāo)題1的TO-DO任務(wù)后回到列表頁->斷言標(biāo)題1存在 -
editTask
添加標(biāo)題1的TO-DO任務(wù)后回到列表頁->點擊此Item進(jìn)入查看頁面->點擊編輯按鈕->修改成標(biāo)題2->點擊保存->斷言標(biāo)題1不存在和標(biāo)題2存在 -
markTaskAsComplete
添加任務(wù)并點擊CheckBox設(shè)置為已完成->通過過濾器進(jìn)入All/Active/Completed視圖,斷言該任務(wù)是否存在 -
markTaskAsActive
測試標(biāo)記任務(wù)為Active狀態(tài),手法同上一點 -
showAllTasks
添加2個任務(wù)->進(jìn)入All視圖->斷言兩個任務(wù)在界面上存在 -
showActiveTasks
添加2個任務(wù)->進(jìn)入Active視圖->斷言兩個任務(wù)在界面上存在 -
showCompletedTasks
添加2個任務(wù)->標(biāo)記為已完成->進(jìn)入Completed視圖->斷言兩個任務(wù)在界面上存在 -
clearCompletedTasks
添加2個任務(wù)->標(biāo)記為已完成->點擊Clear completed按鈕->斷言兩個任務(wù)不存在 -
createOneTask_deleteTask
添加1個任務(wù)->點擊該任務(wù)進(jìn)入詳情頁->點擊刪除->斷言該任務(wù)不存在 -
createTwoTasks_deleteOneTask
創(chuàng)建2個任務(wù)->刪除第2個->斷言第1個存在且第2個不存在 -
markTaskAsCompleteOnDetailScreen_taskIsCompleteInList
創(chuàng)建1個任務(wù)->點擊該任務(wù)進(jìn)入詳情頁->標(biāo)記為已完成->回到列表頁->斷言該任務(wù)為選中狀態(tài) -
markTaskAsActiveOnDetailScreen_taskIsActiveInList
創(chuàng)建1個任務(wù)->在列表頁標(biāo)記為已選中->進(jìn)入詳情頁標(biāo)記為未選中->回到列表頁->斷言該任務(wù)未被選中 -
markTaskAsAcompleteAndActiveOnDetailScreen_taskIsActiveInList
創(chuàng)建1個任務(wù)->在詳情頁觸發(fā)兩次checkbox的點擊- 回到列表頁->斷言該任務(wù)未被選中 -
markTaskAsActiveAndCompleteOnDetailScreen_taskIsCompleteInList
創(chuàng)建1個任務(wù)->標(biāo)記為已完成->在詳情頁觸發(fā)兩次checkbox的點擊- 回到列表頁->斷言該任務(wù)被選中 -
orientationChange_FilterActivePersists
創(chuàng)建1個任務(wù)->標(biāo)記為已完成->進(jìn)入Active視圖->驗證該任務(wù)不存在->切換橫豎屏->斷言該任務(wù)狀態(tài)與之前一致 -
orientationChange_FilterCompletedPersists
創(chuàng)建1個任務(wù)->標(biāo)記為已完成->進(jìn)入Completed視圖->驗證該任務(wù)存在->切換橫豎屏->斷言該任務(wù)狀態(tài)與之前一致
M層:本地數(shù)據(jù)庫操作測試——TasksLocalDataSourceTest
概述:對數(shù)據(jù)庫中處理Task的增刪改查、改變Task狀態(tài)等進(jìn)行測試。
意義:持久層的CRUD往往需要配合起來測試和斷言,此例很好的詮釋了這一點
saveTask_retrievesTask
- 測試目的:驗證保存Task到數(shù)據(jù)庫的邏輯
- 測試用例:實例化Task對象->保存入庫->根據(jù)ID獲取Task->在回調(diào)函數(shù)中斷言與入庫的Task一致
completeTask_retrievedTaskIsComplete
- 測試目的:驗證將任務(wù)設(shè)置成完成狀態(tài)的邏輯
- 測試用例:Task對象保存入庫->觸發(fā)完成任務(wù)的邏輯->根據(jù)ID獲取Task->在回調(diào)函數(shù)中斷言該Task已完成
activateTask_retrievedTaskIsActive
- 測試目的:驗證將任務(wù)設(shè)置為激活狀態(tài)的邏輯
- 測試用例:mock一個回調(diào)對象callback->Task對象保存入庫->觸發(fā)完成任務(wù)的邏輯->觸發(fā)激活任務(wù)的邏輯->根據(jù)ID獲取Task->斷言callback執(zhí)行了onTaskLoaded的邏輯
clearCompletedTask_taskNotRetrievable
- 測試目的:驗證清除所有已完成任務(wù)的邏輯
- 測試用例:mock三個回調(diào)函數(shù)對象,分別是callback1到3->保存任務(wù)1,任務(wù)2和任務(wù)3,其中任務(wù)1和任務(wù)2為completed狀態(tài),任務(wù)3為active狀態(tài)->清理所有已完成的任務(wù)->根據(jù)3個Task的ID分別獲取Task->斷言callback1和callback2執(zhí)行了onDataNotAvailable邏輯- >斷言callback3執(zhí)行onTaskLoaded邏輯
deleteAllTasks_emptyListOfRetrievedTask
- 測試目的:驗證刪除數(shù)據(jù)庫中所有任務(wù)的邏輯
- 測試用例:保存任務(wù)->mock一個回調(diào)函數(shù)callback->刪除所有任務(wù)->獲取任務(wù)列表->斷言callback執(zhí)行了onDataNotAvailable的邏輯
getTasks_retrieveSavedTasks
- 測試目的:驗證獲取數(shù)據(jù)庫中所有任務(wù)的邏輯
- 測試用例:保存2個任務(wù)->獲取任務(wù)列表->在回調(diào)函數(shù)中斷言這2個任務(wù)存在
三 androidTestMock文件下的測試
在《解讀》一文中,筆者提到該文件夾主要的作用是對網(wǎng)絡(luò)請求進(jìn)行Fake,即不發(fā)出網(wǎng)絡(luò)請求,而是返回事先定義好的數(shù)據(jù)。
V層:新增編輯任務(wù)界面測試——AddEditTaskScreenTest
errorShownOnEmptyTask
- 測試目的:驗證保存或編輯任務(wù)時,如果輸入空標(biāo)題,會彈出Snackbar提示不能為空
- 測試用例:打開詳情頁->輸入空標(biāo)題和空描述->點擊保存->通過Snackbar的消息內(nèi)容驗證Snackbar已顯示
V層:統(tǒng)計界面測試——StatisticsScreenTest
-
Tasks_ShowsNonEmptyMessage
打開統(tǒng)計界面->事先Fake兩條任務(wù)數(shù)據(jù),狀態(tài)分別為Completed和Active->斷言兩種統(tǒng)計欄目都已顯示
V層:任務(wù)詳情界面測試——TaskDetailScreenTest
概述:Fake出不同狀態(tài)的任務(wù)并在詳情頁進(jìn)行標(biāo)題、描述和狀態(tài)的斷言。
意義:指導(dǎo)我們?nèi)绾螌W(wǎng)絡(luò)請求數(shù)據(jù)進(jìn)行Fake。
-
activeTaskDetails_DisplayedInUi
Fake一條狀態(tài)為Active的任務(wù)->打開詳情頁->斷言標(biāo)題、描述、任務(wù)狀態(tài) -
completedTaskDetails_DisplayedInUi
Fake一條狀態(tài)為Complete的任務(wù)->打開詳情頁->斷言標(biāo)題、描述、任務(wù)狀態(tài) -
orientationChange_menuAndTaskPersist
橫豎屏的測試手法與TasksScreenTest
中一致,不再贅述。
四 test文件夾下的測試
P層:新增編輯任務(wù)測試——AddEditTaskPresenterTest
概述:進(jìn)入Presenter層的測試后,我們不再去斷言輸入輸出了,取而代之的是,斷言是否正確的覆蓋了View層和Model層的邏輯。
AddEditTaskPresenter
共有三個方法,分別是createTask
、updateTask
和populateTask
,對應(yīng)增加、修改、展示任務(wù)的功能,其中增加任務(wù)涉及到成功和失敗的情況,因此有4個測試用例。
意義:這些Presenter層的測試可以教會我們?nèi)绾蜯ock,如何verify,如何測試異步回調(diào),以及如何完整的覆蓋Presenter層的所有邏輯路徑。
-
saveNewTaskToRepository_showsSuccessMessageUi
創(chuàng)建Presenter,執(zhí)行創(chuàng)建任務(wù)的邏輯->斷言Model層執(zhí)行了保存的邏輯->斷言View層執(zhí)行了顯示任務(wù)列表的邏輯 -
saveTask_emptyTaskShowsErrorUi
創(chuàng)建Presenter,執(zhí)行創(chuàng)建任務(wù)的邏輯,且任務(wù)Title為空->斷言View層執(zhí)行了展示error的邏輯 -
saveExistingTaskToRepository_showsSuccessMessageUi
此用例驗證的是update任務(wù)的邏輯,測試手法同1。 populateTask_callsRepoAndUpdatesView
- 測試目的:驗證詳情頁展示的任務(wù)信息是否正確
- 測試用例:presenter執(zhí)行populateTask()->斷言執(zhí)行了getTask(),且參數(shù)正確->斷言回調(diào)函數(shù)執(zhí)行了正確的邏輯->斷言View層展示的是正確的Task數(shù)據(jù)
P層:統(tǒng)計功能測試——StatisticsPresenterTest
概述:該類的presenter接口比較簡單,只有一個入口方法
start
,執(zhí)行的是加載統(tǒng)計信息的邏輯,執(zhí)行過程中涉及幾個路徑:加載空任務(wù)列表,加載非空任務(wù)列表和數(shù)據(jù)不可用,分別對應(yīng)以下1,2,3點。
-
loadEmptyTasksFromRepository_CallViewToDisplay
斷言加載空任務(wù)列表 -
loadNonEmptyTasksFromRepository_CallViewToDisplay
斷言加載非空任務(wù)列表 -
loadStatisticsWhenTasksAreUnavailable_CallErrorToDisplay
斷言數(shù)據(jù)不可用
P層:任務(wù)詳情功能測試——TaskDetailPresenterTest
概述:該Presenter共有5個方法,分別是:
-
start
:展示任務(wù)詳情,涉及三種路徑:展示Active任務(wù)、展示Compeled任務(wù)和展示非法ID的任務(wù),對應(yīng)1,2,3的測試用例 -
deleteTask
:刪除任務(wù),對應(yīng)第4個測試用例 -
completeTask
:完成任務(wù),對于第5個 -
activateTask
:激活任務(wù),對應(yīng)第6個 -
editTask
:編輯任務(wù),對應(yīng)第7個,編輯非法ID的任務(wù)對應(yīng)的測試用例為第8個
getActiveTaskFromRepositoryAndLoadIntoView
getCompletedTaskFromRepositoryAndLoadIntoView
getUnknownTaskFromRepositoryAndLoadIntoView
deleteTask
completeTask
activateTask
activeTaskIsShownWhenEditing
invalidTaskIsNotShownWhenEditing
P層:任務(wù)列表功能測試——TasksPresenterTest
概述,此TasksPresenter的測試與上一點類似,從接口方法出發(fā),此類共有10個接口方法,為此設(shè)計了8個測試用例,分別是展示All/Active/Completed的任務(wù)列表、點擊打開任務(wù)詳情頁、改變?nèi)蝿?wù)狀態(tài)等。
loadAllTasksFromRepositoryAndLoadIntoView
loadActiveTasksFromRepositoryAndLoadIntoView
loadCompletedTasksFromRepositoryAndLoadIntoView
clickOnFab_ShowsAddTaskUi
clickOnTask_ShowsDetailUi
completeTask_ShowsTaskMarkedComplete
activateTask_ShowsTaskMarkedActive
unavailableTasks_ShowsError
M層:數(shù)據(jù)操作門面類測試——TasksRepositoryTest
概述:該類的測試用例非常齊全,對于如何設(shè)計測試用例讓數(shù)據(jù)過期,如何讓數(shù)據(jù)取自本地或者網(wǎng)絡(luò)等測試技巧都有極高的學(xué)習(xí)價值。
getTasks_repositoryCachesAfterFirstApiCall
getTasks_requestsAllTasksFromLocalDataSource
saveTask_savesTaskToServiceAPI
completeTask_completesTaskToServiceAPIUpdatesCache
completeTaskId_completesTaskToServiceAPIUpdatesCache
activateTask_activatesTaskToServiceAPIUpdatesCache
activateTaskId_activatesTaskToServiceAPIUpdatesCache
getTask_requestsSingleTaskFromLocalDataSource
deleteCompletedTasks_deleteCompletedTasksToServiceAPIUpdatesCache
deleteAllTasks_deleteTasksToServiceAPIUpdatesCache
deleteTask_deleteTaskToServiceAPIRemovedFromCache
getTasksWithDirtyCache_tasksAreRetrievedFromRemote
getTasksWithLocalDataSourceUnavailable_tasksAreRetrievedFromRemote
getTasksWithBothDataSourcesUnavailable_firesOnDataUnavailable
getTaskWithBothDataSourcesUnavailable_firesOnDataUnavailable
getTasks_refreshesLocalDataSource