Android Unit Test實踐

為什么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
image.png
  • 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!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末贝乎,一起剝皮案震驚了整個濱河市情连,隨后出現的幾起案子,更是在濱河造成了極大的恐慌览效,老刑警劉巖却舀,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虫几,死亡現場離奇詭異,居然都是意外死亡挽拔,警方通過查閱死者的電腦和手機辆脸,發(fā)現死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篱昔,“玉大人每强,你說我怎么就攤上這事≈莨簦” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵浪箭,是天一觀的道長穗椅。 經常有香客問我,道長奶栖,這世上最難降的妖魔是什么匹表? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮宣鄙,結果婚禮上袍镀,老公的妹妹穿的比我還像新娘。我一直安慰自己冻晤,他們只是感情好苇羡,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鼻弧,像睡著了一般设江。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上攘轩,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天叉存,我揣著相機與錄音,去河邊找鬼度帮。 笑死歼捏,一個胖子當著我的面吹牛,可吹牛的內容都是我干的笨篷。 我是一名探鬼主播瞳秽,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼冕屯!你這毒婦竟也來了寂诱?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤安聘,失蹤者是張志新(化名)和其女友劉穎痰洒,沒想到半個月后瓢棒,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡丘喻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年脯宿,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泉粉。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡连霉,死狀恐怖,靈堂內的尸體忽然破棺而出嗡靡,到底是詐尸還是另有隱情跺撼,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布讨彼,位于F島的核電站歉井,受9級特大地震影響,放射性物質發(fā)生泄漏哈误。R本人自食惡果不足惜哩至,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蜜自。 院中可真熱鬧菩貌,春花似錦、人聲如沸重荠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晚缩。三九已至尾膊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荞彼,已是汗流浹背冈敛。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸣皂,地道東北人抓谴。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像寞缝,于是被迫代替她去往敵國和親癌压。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容

  • 什么是單元測試 在計算機編程中荆陆,單元測試(Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設計的最...
    HelloCsl閱讀 10,956評論 1 46
  • @Author:彭海波 前言 單元測試(又稱為模塊測試, Unit Testing)是針對程序模塊(軟件設計的最小...
    海波筆記閱讀 4,964評論 0 52
  • 一.基本介紹 背景: 目前處于高速迭代開發(fā)中的Android項目往往需要除黑盒測試外更加可靠的質量保障滩届,這正是單元...
    anmi7閱讀 2,026評論 0 6
  • Android單元測試介紹 處于高速迭代開發(fā)中的Android項目往往需要除黑盒測試外更加可靠的質量保障,這正是單...
    東經315度閱讀 3,112評論 6 37
  • 在博客Android單元測試之JUnit4中被啼,我們簡單地介紹了:什么是單元測試帜消,為什么要用單元測試棠枉,并展示了一個簡...
    水木飛雪閱讀 9,453評論 4 18