為什么Android Unit Test在項目團隊中沒有普遍應用盲赊,主要原因還是Android Api的調用依賴設備,另外一部分是除了ui代碼外純邏輯的代碼不多敷扫,這篇文章主要針對困難哀蘑,提供其解決方案,方便大家在項目中用起Unit Test葵第。
Android Unit Test的常見問題
- 異步任務執(zhí)行測試绘迁;
- 項目代碼解偶不徹底,某方法的邊界很多或不好在真實場景下創(chuàng)造卒密;
- 靜態(tài)方法不好mock缀台;
- Kotlin中的類和方法沒有默認open,無法mock
- Kotlin中只讀變量無法mock哮奇;
- Adnroid項目中有很多和設備相關的Api膛腐,比如Context,Environment等等。
下面針對這些問題一一分析鼎俘。
Android Unit Test ”Hello World"
- junit configure
dependencies junit aar in gradle:
testImplementation 'junit:junit:4.12'
創(chuàng)建單元測試類
junit test目錄在src目錄下(即與main在同一目錄)哲身,名字為test,如果沒有可以手動創(chuàng)建目錄贸伐。
創(chuàng)建對應類的Junit test類勘天,在類代碼中,在File文件中選中被測類名捉邢,右擊 -> Generate -> Test脯丝,填寫類名和勾選測試方法即可,點擊Ok伏伐,會提示選test還是AndroidTest宠进,選test點OK,Android Studio會在test對應目錄下創(chuàng)建Test類藐翎。代碼:被測類UnitTestHelloWorld.kt
class UnitTestHelloWorld {
fun add(a: Int, b: Int): Int {
return a + b
}
}
- 代碼:測試類UnitTestHelloWorldTest.kt
class UnitTestHelloWorldTest {
// 這個方法有個注解砰苍,表示一個Unit Test
@Test
fun add() {
val result = UnitTestHelloWorld().add(10, 10)
assertEquals(20, result)
}
}
運行Unit Test
兩種方式運行:
一. 批量運行test潦匈,右擊左邊Project欄下對應的類文件或對應包名,選中類名會運行該類所有test赚导,選中包名會運行包下面所有類的test,右擊后選擇"Run "Tests in xxxx""即可赤惊,在Run View中可以看到Test運行結果和輸出吼旧。
二. 運行單個test,在Test類文件中左邊行號附近有個運行按鈕未舟,點擊即可運行單個test圈暗。
運行結果會在Run窗口中顯示,信息包括運行了多少個test裕膀,多少個通過员串,多少個不通過,不通過的是哪些昼扛。
Debug Unit Test
在對應的代碼中添加斷點寸齐,和運行操作一樣,運行彈窗選擇中的Debug即可抄谐。
異步任務執(zhí)行測試
對異步任務執(zhí)行進行測試時渺鹦,如果單元測試方法中不做處理,單元測試會一直執(zhí)行到方法底部而結束蛹含,并不會等待異步任務執(zhí)行完毅厚,處理異步等待的一個比較好的方式是通過CountDownLatch類來執(zhí)行等待,該類不僅可以等待浦箱,還可以設置等待的任務數量吸耿。
線程池異步執(zhí)行類:
class SingleThreadAsyncHelper private constructor(){
companion object {
val sInstance: SingleThreadAsyncHelper by lazy (mode = LazyThreadSafetyMode.SYNCHRONIZED) { SingleThreadAsyncHelper() }
}
private val mExecutor: ThreadPoolExecutor = ThreadPoolExecutor(1, 1,
10 * 60, TimeUnit.SECONDS,
LinkedBlockingQueue<Runnable>())
init {
mExecutor.allowCoreThreadTimeOut(true)
}
fun <T> submitTask(taskAction: () -> T): Future<T> {
return mExecutor.submit(Callable { taskAction.invoke() })
}
}
Test類:
class SingleThreadAsyncHelperTest {
@Test
fun submitTask() {
// 異步同步信號,設置等待的信號數量為1
val signal = CountDownLatch(1)
var value = 0
// 異步執(zhí)行酷窥,與測試線程不是一個
SingleThreadAsyncHelper.sInstance.submitTask {
Thread.sleep(2000)
value++
// 減少等待的信號數量
signal.countDown()
}
// 線程等待咽安,直到信號量為0
signal.await()
// 得到測試結果
assertEquals(1, value)
}
}
項目代碼解偶不徹底,某方法的邊界很多或不好在真實場景下創(chuàng)造
當然可測性是代碼設計的一個重要參考項竖幔,但是無論項目設計多好都會有依賴板乙,某些依賴或復雜場景無法顯示創(chuàng)造,我們可以對一些依賴和一些復雜場景進行模擬拳氢,設置任何我們想要的場景募逞,我們采用Mockito庫,下面對一個提交很對文件的任務進行測試來介紹Mockito馋评,注意一下的Test不能直接運行放接。
- 引用Mockito庫的依賴
testImplementation "org.mockito:mockito-core:2.23.0"
class FinishTaskTest {
private val questionStatus = QuestionSetStatus()
@get:Rule
public var rule = PowerMockRule()
@Before
fun setUp() {
val baseUrl = MockRetrofit.BASE_URL_SUBMIT_FINISH
// 1--這部分后面再講
PowerMockito.mockStatic(Env::class.java)
// 這是mock Env.getBaseUrl()的返回值為我們自定義的地址
Mockito.`when`(Env.getBaseUrl()).thenReturn(baseUrl)
// 2--這部分后面再講
PowerMockito.mockStatic(APIService::class.java)
// 這是mock Retrofit請求類,MockRetrofit里面我們自己根據url自定義了返回結果 Mockito.`when`(APIService.createRxService(HomeworkApi::class.java)).thenReturn(
MockRetrofit.getMockService(
HomeworkApi::class.java, baseUrl))
}
@Test
fun getTask() {
val finishTask = FinishTask(0.8f, 0.2f)
// 這是異步等待接口提交
val disposableAndProgress = doFinishTaskAwait(finishTask)
assertEquals(100, disposableAndProgress.second)
}
}
Mockito使用比較簡單留特,其他api使用和實現原理可以參考Mockito官網和Mockito源碼纠脾。
靜態(tài)方法不好mock
上面提的Mockito庫是無法mock靜態(tài)方法的背率,如果要mock靜態(tài)方法序矩,我們可以使用PowerMockito。
- 引入PowerMockito lib
testImplementation "org.powermock:powermock-module-junit4:1.6.6"
testImplementation "org.powermock:powermock-module-junit4-rule:1.6.6"
testImplementation "org.powermock:powermock-api-mockito:1.6.6"
testImplementation "org.powermock:powermock-classloading-xstream:1.6.6"
- Mock Static方法,直接用前面的網絡請求的mock案例分析
@RunWith(PowerMockRunner::class) // 設置Runner
@PrepareForTest(ApiService::class,
APIService::class, // 設置需要mock static的類
Env::class)
class SubjectiveALiYunAllFileTaskTest {
// 1.實踐發(fā)現還需要加這一行
@get:Rule
public var rule = PowerMockRule()
@Before
fun setUp() {
val baseUrl = MockRetrofit.BASE_URL_SUBMIT_FINISH
// 2.對static方法進行mock架专,只有經過這行,下面的mock才有效
PowerMockito.mockStatic(Env::class.java)
Mockito.`when`(Env.getBaseUrl()).thenReturn(baseUrl)
// 3.對static方法進行mock酬滤,只有經過這行仅炊,下面的mock才有效
PowerMockito.mockStatic(APIService::class.java)
Mockito.`when`(APIService.createRxService(HomeworkApi::class.java)).thenReturn(
MockRetrofit.getMockService(
HomeworkApi::class.java, baseUrl))
}
@Test
fun getTask() {
val finishTask = FinishTask(0.8f, 0.2f)
// 4.這是異步等待接口提交
val disposableAndProgress = doFinishTaskAwait(finishTask)
assertEquals(100, disposableAndProgress.second)
}
}
PowerMockito的其他使用請自我查看文檔PowerMockito源碼和文檔
Kotlin中的類和方法沒有默認open,無法mock
默認情況下Mocktio對于final的類和方法不能mock菱鸥,而Kotlin如果沒有添加open修飾默認是final的宗兼,這樣就會出現很多類和方法是final的,解決該問題是添加一個Mocktio的配置氮采,操作如下:
- 在添加配置文件test/resources/mockito-extensions/org.mockito.plugins.MockMaker文件殷绍,在文件中添加:
mock-maker-inline
- Mocktio版本使用2.0以上
私有變量或Kotlin中只讀變量無法mock
對于這種情況可以采用反射的方式實現。
上案例:
數據庫操作類AsyncAndOrderHomeworkDbManager:
class AsyncAndOrderHomeworkDbManager private constructor(){
companion object {
val sInstance: AsyncAndOrderHomeworkDbManager by lazy (mode = LazyThreadSafetyMode.SYNCHRONIZED) {
AsyncAndOrderHomeworkDbManager()
}
/**
* 初始化數據庫
*/
fun initDB(context: Context) {
QuestionDatabaseHelper.initDB(context.applicationContext)
}
}
// 1.需要mock以下兩個變量
@VisibleForTesting
private val mQuestionSetStatusDao: QuestionSetStatusDao = QuestionDatabaseHelper.getQuestionSetDao()
@VisibleForTesting
private val mQuestionAnswerDao = QuestionDatabaseHelper.getQuestionAnswerDao()
}
實現的反射類ReflectionTestUtils:
object ReflectionTestUtils {
@Throws(Exception::class)
fun setField(objectBean: Any, propertyName: String, newValue: Any?) {
//獲得ReflectPoint類中的一個屬性str1
val field = objectBean.javaClass.getDeclaredField(propertyName)
//強制獲取屬性中的值(私有屬性不能輕易獲取其值)
field.isAccessible = true
System.out.println(field.get(objectBean))
//修改屬性的值
field.set(objectBean, newValue)
}
}
測試類:
@RunWith(RobolectricTestRunner::class)
@PowerMockIgnore("org.mockito.*", "org.robolectric.*", "android.*")
@PrepareForTest(AsyncAndOrderHomeworkDbManager::class)
class AsyncAndOrderHomeworkDbManagerTest {
@Before
fun setUp() {
// 1.反射修改私有變量
ReflectionTestUtils.setField(AsyncAndOrderHomeworkDbManager.sInstance, "mQuestionSetStatusDao",
QuestionDatabaseHelper.getQuestionSetDao())
ReflectionTestUtils.setField(AsyncAndOrderHomeworkDbManager.sInstance, "mQuestionAnswerDao",
QuestionDatabaseHelper.getQuestionAnswerDao())
}
}
當然像這種反射工具類和上面的RetrofitMock類MockRetrofit可以在平常的實踐中慢慢積累鹊漠,之后遇到類似工具類可以直接用主到。
Adnroid項目中有很多和設備相關的Api,比如Context,Environment等等贸呢,導致很多地方無法運行單元測試
Android項目中對設備的依賴就是因為android.jar镰烧,開發(fā)引用的android.jar中的實現很多都是throw RuntimeException,具體實現會在app安裝到設備上時楞陷,使用設備上的android.jar怔鳖。Robolectric正是在這種環(huán)境下誕生的開源Android單元測試框架。Robolectric自己實現了Android啟動的相關庫固蛾,例如Application结执、Acticity等,我們可以通過activityController.create()來啟動一個activity艾凯,除此之外還有文件系統(tǒng)等献幔。
- 引入Robolectric lib
testImplementation 'org.robolectric:robolectric:3.0'
- 在Test中使用,已測試數據庫讀寫為案例
@RunWith(RobolectricTestRunner::class) // 1.配置Runner
@PowerMockIgnore("org.mockito.*", "org.robolectric.*", "android.*")// 2.這是PowerMock和Robolectric沖突的點
@PrepareForTest(AsyncAndOrderHomeworkDbManager::class)
class AsyncAndOrderHomeworkDbManagerTest {
private val questionSetStatus = QuestionSetStatus().apply {
questionSetId = 1
questionSetType = 1
uid = 1
name = "questionSetStatus"
}
@Before
fun setUp() {
// 3.初始化數據庫趾诗,這里的RuntimeEnvironment是Robolectric提供
QuestionDatabaseHelper.initDB(RuntimeEnvironment.application)
// 4.應用新的數據庫對象
// 5.反射修改對數據庫引用的property蜡感,因為每執(zhí)行一個test開始時都會調用下@Before[setUp()]和執(zhí)行結束時都會調用@After[tearDown],
// 6.所以避免數據庫被重復打開需要結束時關閉以下,同時單例中引用的數據庫對象也需要改變恃泪。 ReflectionTestUtils.setField(AsyncAndOrderHomeworkDbManager.sInstance, "mQuestionSetStatusDao",
QuestionDatabaseHelper.getQuestionSetDao())
ReflectionTestUtils.setField(AsyncAndOrderHomeworkDbManager.sInstance, "mQuestionAnswerDao",
QuestionDatabaseHelper.getQuestionAnswerDao())
}
@After
fun tearDown() {
// 7.一個test結束郑兴,關閉數據庫對象
QuestionDatabaseHelper.getDB().close()
}
@Test
fun asyncGetQuestionSet() {
// Test處理異步的測試
val signal = CountDownLatch(1)
// 寫數據庫
AsyncAndOrderHomeworkDbManager.sInstance.asyncSaveOrUpdateQuestionSetWait(questionSetStatus)
var getQuestionSetStatus: QuestionSetStatus? = null
// 讀數據庫
AsyncAndOrderHomeworkDbManager.sInstance.asyncGetQuestionSet(1, 1, 1)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe ({
// 把異步的執(zhí)行結果保存
getQuestionSetStatus = it
// 通知異步等待結束
signal.countDown()
}, {
System.out.println(Log.getStackTraceString(it))
signal.countDown()
},{
signal.countDown()
})
// 等待執(zhí)行完成
signal.await()
Assert.assertEquals("questionSetStatus", getQuestionSetStatus?.name)
}
}
End!