国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

編寫你的第一個(gè) Android 單元測試

番茄西紅柿 / 3629人閱讀

摘要:什么是單元測試單元測試是對(duì)程序的最小單元進(jìn)行正確性檢驗(yàn)的測試工作。編寫第一個(gè)單元測試單元測試主要使用是測試框架類庫的擴(kuò)展庫,需要在中聲明測試依賴。目標(biāo)代碼這里以一個(gè)簡單的中的例子來說明如何寫單元測試。

TL;DR: 本文主要面向單元測試新手,首先簡單介紹了什么是單元測試,為什么要寫單元測試,討論了一下 Android 項(xiàng)目中哪些代碼適合做單元測試,并以一個(gè)簡單例子演示了如何編寫屬于你的第一個(gè) Android 單元測試(kotlin 代碼)。

什么是單元測試

單元測試是對(duì)程序的最小單元進(jìn)行正確性檢驗(yàn)的測試工作。程序單元是應(yīng)用的最小可測試部件。一個(gè)單元可能是單個(gè)程序、類、對(duì)象、方法等。 —— Wikipedia

為什么要做單元測試

沒有測試的代碼都是不可靠的?!?魯迅

驗(yàn)證代碼正確性,增強(qiáng)對(duì)代碼的信心

最直接的好處。在沒有單元測試的時(shí)候,通常我們自測的方法就是跑一跑程序,簡單構(gòu)造一下主要的分支場景,如果通過了,就認(rèn)為 OK 可以提交給 QA 同學(xué)了。但實(shí)際上有些時(shí)候有些分支自己是無法測到或者很難構(gòu)造出來?xiàng)l件的,這只能依靠 QA 同學(xué)手工測試來覆蓋,如果他們也沒有測到,那只能老天保佑了。而通過單元測試我們可以方便構(gòu)造各種測試場景,對(duì)于通過測試的代碼,我們會(huì)更有信心

在不需要 QA 參與的情況下保持或改進(jìn)產(chǎn)品質(zhì)量

說白了就是可以放心的重構(gòu)。QA 同學(xué)總是談重構(gòu)而色變,我們?cè)谥貥?gòu)遺留代碼的時(shí)候也是提心吊膽,生怕改錯(cuò)了舊的邏輯,或者意外影響到別的模塊。有了單元測試,我們就可以更加大膽的進(jìn)行重構(gòu),重構(gòu)完只要跑一下單測驗(yàn)證是否通過就可以了(適合小范圍的重構(gòu),大的重構(gòu)可能就需要重寫單元測試了)

加深對(duì)業(yè)務(wù)理解

在設(shè)計(jì)測試用例的過程中,需要考慮到業(yè)務(wù)上的各種場景,有助于我們跳出代碼加深對(duì)業(yè)務(wù)的理解

幫你寫出更好的代碼

單元測試要求被測試的代碼高內(nèi)聚,低耦合,所以你在寫業(yè)務(wù)代碼的時(shí)候就要考慮到如何寫測試,或者反過來,先寫測試用例的話會(huì)讓你能夠?qū)懗鰜斫Y(jié)構(gòu)性更好的代碼

單元測試有什么代價(jià)嗎?當(dāng)然也是有的,編寫和維護(hù)測試用例需要花費(fèi)一定的時(shí)間和精力,當(dāng)項(xiàng)目進(jìn)度壓力比較大的時(shí)候,很多人是不愿意再花時(shí)間去寫測試的。這就需要進(jìn)行權(quán)衡,要么不寫然后喪失前面說的各種好處,要么后面有時(shí)間再補(bǔ)上來,但也錯(cuò)過了寫測試的最好時(shí)間。

Android 單元測試

Android 項(xiàng)目默認(rèn)會(huì)創(chuàng)建兩個(gè)測試目錄,分別為 src/test 和 src/androidTest 前者是單元測試目錄,后者是依賴 Android 框架的 instrumentation 測試目錄。聲明測試也有區(qū)別,前者是 testImplementation 后者是 androidTestImplementation,我們今天討論的是前者,也叫 Local Unit Test,意思也就是說不依賴 Android 真機(jī)或者模擬器,可以直接在本地 JVM 上運(yùn)行的單元測試。

Android 的單元測試與普通的 java 項(xiàng)目并沒有太大差異,首先需要關(guān)注的是如何分辨那些類或者方法需要測試。

一個(gè)好的單元測試的一個(gè)重要特性就是運(yùn)行速度要快,通常是毫秒級(jí)的,而依賴 Android 框架的代碼都需要在模擬器上或者真機(jī)上運(yùn)行(也不是絕對(duì)的),速度不可避免的會(huì)慢很多,所以我們?cè)谧?Android 單元測試的時(shí)候會(huì)避免讓被測試代碼對(duì) Android 框架有任何依賴。在這個(gè)條件下,一般適合進(jìn)行單元測試的代碼就是:

    MVP 結(jié)構(gòu)中的 Presenter 或者 MVVM 結(jié)構(gòu)中的 ViewModel

    Helper 或者 Utils 工具類

    公共基礎(chǔ)模塊,比如網(wǎng)絡(luò)庫、數(shù)據(jù)庫等

如果你的項(xiàng)目中代碼與 Android 框架耦合比較高,那么可能就不得不先對(duì)目標(biāo)代碼進(jìn)行重構(gòu),然后再編寫測試代碼。如何重構(gòu)不在本文討論范圍,請(qǐng)自行探索。

編寫第一個(gè) Android 單元測試 SETUP

Android 單元測試主要使用是 JUnit 測試框架 + Mockito Mock 類庫 + Mockito-kotlin 的擴(kuò)展庫,需要在 build.gradle 中聲明測試依賴。后面的示例代碼對(duì)應(yīng)的依賴如下。

testImplementation junit:junit:4.12
testImplementation org.mockito:mockito-core:2.19.0
testImplementation com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0

具體每個(gè)庫是用來做什么的,后面根據(jù)具體的代碼來說明。

目標(biāo)代碼

這里以一個(gè)簡單的 MVP 中 Presenter 的例子來說明如何寫單元測試。 以下測試代碼來自于這里,是一個(gè)食譜搜索結(jié)果展示頁面。

class SearchResultsPresenter(private val repository: RecipeRepository) :
    BasePresenter() {
  private var recipes: List);null

  fun search(query: String) {
    view);object : RecipeRepository.RepositoryCallback> {
      override fun onSuccess(recipes: List<Recipe>); {
        this@SearchResultsPresenter.recipes = recipes
        if (recipes != null && recipes.isNotEmpty()) {
          view);else {
          view);override fun onError() {
        view);fun addFavorite(recipe: Recipe) {
    recipe.isFavorited = true

    repository.addFavorite(recipe)

    val recipeIndex = recipes);if (recipeIndex != null) {
      view);fun removeFavorite(recipe: Recipe) {
    repository.removeFavorite(recipe)
    recipe.isFavorited = false
    val recipeIndex = recipes);if (recipeIndex != null) {
      view);interface View {
    fun showLoading()
    fun showRecipes(recipes: List<Recipe>)
    fun showEmptyRecipes()
    fun showError()
    fun refreshFavoriteStatus(recipeIndex: Int)
  }
}

簡單分析一下代碼。

首先這個(gè) Presenter 類包含了一個(gè)內(nèi)部類 View ,定義了 MVP 中 View 應(yīng)該實(shí)現(xiàn)的一些方法,包括顯示加載狀態(tài),顯示食譜列表,顯示空頁面,顯示錯(cuò)誤頁面,刷新最愛等接口方法。

它的構(gòu)造函數(shù)接受了一個(gè) RecipeRepository 對(duì)象,我們來看一下 RecipeRepository 的定義。

interface RecipeRepository {
  fun addFavorite(item: Recipe)
  fun removeFavorite(item: Recipe)
  fun getFavoriteRecipes(): List
  fun getRecipes(query: String, callback: RepositoryCallback<List<Recipe>>)
}

interface RepositoryCallback<in T> {
  fun onSuccess(t: T);
  fun onError()
}

可以看到它是也是一個(gè)接口類,顧名思義它是一個(gè) recipe 的數(shù)據(jù)倉庫,定義了一系列的數(shù)據(jù)獲取和更新接口,至于從哪里獲取并不需要我們不關(guān)心,可以是本地文件、數(shù)據(jù)庫、網(wǎng)絡(luò)等等。這也正是依賴翻轉(zhuǎn)原則的體現(xiàn)。

這個(gè) Presenter 又繼承了 BasePresenter,這個(gè)類是一個(gè)抽象類,定義了兩個(gè)方法,分別是 attachView() 和 detachView(),還有一個(gè)字段 view。

abstract class BasePresenter<V> {
  protected var view: V);null

  fun attachView(view: V) {
    this.view = view
  }

  fun detachView() {
    this.view = null
  }
}

回到 SearchResultsPresenter 自身,這個(gè)類有三個(gè)主要方法,第一個(gè) search() 接受一個(gè)字符串,調(diào)用了 repository 的方法獲取搜索結(jié)果,根據(jù)結(jié)果分別調(diào)用 View 的不同方法;第二個(gè) addFavorite(),它接受一個(gè) recipe 對(duì)象,將其設(shè)置為最愛,并調(diào)用 repository 更新到數(shù)據(jù)倉庫中,最后調(diào)用 view 方法刷新 UI;第三個(gè)方法 removeFavorite() ,它與上一個(gè)方法剛好相反?;惖姆椒ú辉谖覀儨y試范圍內(nèi),不用考慮。

這三個(gè)方法無疑就是我們單元測試的目標(biāo)了,繼續(xù)看如何寫測試代碼。

創(chuàng)建測試類

首先定位到我們要測試的類,使用快捷鍵 CMD + N (Generate),選中 Test,就會(huì)出來一個(gè)彈窗,引導(dǎo)我們創(chuàng)建一個(gè)對(duì)應(yīng)的測試類,類名通常是我們要測試的類 + Test 后綴。要記得位置要放到 src/test 目錄下喲(也可以手動(dòng)定位到相應(yīng)目錄,創(chuàng)建一個(gè)新的文件,但會(huì)慢很多)。

編寫測試代碼

行為驗(yàn)證

首先添加如下代碼

class SearchResultsPresenterTests {

  private lateinit var repository: RecipeRepository
  private lateinit var presenter: SearchResultsPresenter
  private lateinit var view: SearchResultsPresenter.View

  @Before
  fun setup() {
    repository = mock()
    view = mock()
    presenter = SearchResultsPresenter(repository)
    presenter.attachView(view)
  }

解釋一下,這里可能比較陌生的代碼有兩處:

    @Before 注解

這個(gè)注解是 Junit 測試框架的一部分,當(dāng)前測試類中的每一個(gè)測試用例都會(huì)先調(diào)用 @Before 注解的方法,所以可以用來做一些公共的 setup 的操作。具體在這里,我們要測試的是 Presenter,所以就是創(chuàng)建好了一個(gè) Presenter 實(shí)例,并配置了需要與 Presenter 交互的 View / Repository 等外部對(duì)象。與 Before 對(duì)應(yīng),還有一個(gè) @After 注解,可以標(biāo)注一個(gè)方法,用來在每個(gè)用例執(zhí)行完畢后做一些清理操作,如果不需要的話 ,也可以省略不寫。

    mock() 方法

這個(gè)方法是 mockito-kotlin 庫提供的,它是一個(gè)包裝類庫,背后又調(diào)用了 Mockito 類庫,這個(gè)庫可以用來偽造一些穩(wěn)定的依賴類,避免不穩(wěn)定的依賴造成我們的單元測試結(jié)果不可預(yù)期。具體在這里,因?yàn)槲覀儨y試的目標(biāo)是 Presenter 類,與 Presenter 有交互關(guān)系的 View 和 Repo 都有抽象的接口,我們不想測試具體的 View 和 Repo 類(一 View 依賴了 Android 框架,運(yùn)行太慢,二 Repo 可能依賴了網(wǎng)絡(luò)或者數(shù)據(jù)庫或者文件,不夠穩(wěn)定),就可以使用 mock() 方法來創(chuàng)建一個(gè)模擬的類(這里 mock() 是一個(gè)泛型方法,使用了 kotlin 的類型推斷特性)。 Mock 出來的類可以用來檢測對(duì)應(yīng)的方法是否被調(diào)用,調(diào)用了多少次,調(diào)用的次序等等。

接下來添加第一個(gè)測試用例,我們要驗(yàn)證一下調(diào)用 presenter 的 search() 方法后,View 的 showLoading() 方法會(huì)被調(diào)用到。

@Test
fun search_callsShowLoading() {
    presenter.search("eggs")
    verify(view).showLoading()
}

首先當(dāng)然是先調(diào)用 presenter 的 search 方法,然后我們 調(diào)用了一個(gè) verify 方法,它會(huì)接受一個(gè) Mock 的對(duì)象,然后我們就可以驗(yàn)證這個(gè) Mock 對(duì)象的 showLoading() 方法被調(diào)用過了! 很簡單有沒有。在這個(gè)方法聲明的左邊,有一個(gè)運(yùn)行按鈕,點(diǎn)擊就可以執(zhí)行這個(gè)測試用例了(快捷鍵 Ctrl + Shift + R)。

我們?cè)賮韺懸粋€(gè)比較復(fù)雜的測試用例,這次我們要驗(yàn)證一下 search() 調(diào)用后,repo 的 getRecipes() 方法會(huì)調(diào)用到,當(dāng)回調(diào)返回后,view 的 showRecipes() 方法會(huì)調(diào)用到。

@Test
fun search_succeed_callShowRecipes() {
    val recipe = Recipe("id", "title", "imageUrl", "sourceUrl", false)
    val recipes = listOf(recipe)
    doAnswer {
        val callback: RepositoryCallback> = it.getArgument(1)
        callback.onSuccess(recipes)
    }.whenever(repository).getRecipes(eq("eggs"), any())

    presenter.search("eggs")

    verify(repository).getRecipes(eq("eggs"), any())
    verify(view).showRecipes(eq(recipes))
}

喔,這個(gè)方法代碼量一下多了好多,但不要被嚇到,其實(shí)都很好理解,首先我們創(chuàng)建了 recipes 對(duì)象來作為 repo 的搜索的返回結(jié)果,這里我們使用了一個(gè)新的方法,doAnswer{}.whenever().getRecipes(),也很好理解,就是當(dāng)調(diào)用的到 Mock 對(duì)象的 getRecipes() 方法的時(shí)候做一些事情,在 doAnswer{} 方法體中,我們拿到了回調(diào)的對(duì)象,并執(zhí)行了 onSuccess() 回調(diào),將我們構(gòu)造的搜索結(jié)果返回回去(這個(gè)過程就叫做 Stubbing,翻譯過來就是插樁)。好了,到這里位置我們已經(jīng)構(gòu)造好了測試的前提條件,下一步就是調(diào)用 presenter 的 search() 方法了。最后就是驗(yàn)證步驟了,也很好理解,不廢話了。

前面有兩個(gè)方法沒有講到,就是 eq("eggs") 方法和 any() 方法,這兩個(gè)方法返回都是 Matcher 對(duì)象,顧名思義就是用來校驗(yàn)參數(shù)是否與預(yù)期的符合,any() 比較特殊,意思就是我們不在乎到底是什么。需要注意的是,如果在方法調(diào)用時(shí)有一個(gè)參數(shù)使用了 Matcher,所有其他參數(shù)都必須也是 Matcher,這個(gè)不需要自己記住,如果你寫錯(cuò)了,運(yùn)行時(shí)就會(huì)報(bào)錯(cuò)。

根據(jù)前面的例子,很容易就可以聯(lián)想到還可以增加 search 失敗的時(shí)候調(diào)用 view.showError(),以及 search 結(jié)果為空時(shí),調(diào)用 view.showEmpty() 的測試用例,小菜一疊是不是?

前面寫的這些測試用例都是驗(yàn)證被測試對(duì)象依賴的模塊的某些方法可以被正確調(diào)用,所以可以歸為一類叫做行為驗(yàn)證,也就是 Mockito 通常被用來做的事情。

狀態(tài)驗(yàn)證

還有一類測試,叫做狀態(tài)驗(yàn)證,通常使用 JUnit 庫中的 Assert 函數(shù),我們也舉一個(gè)例子。presenter 中有一個(gè)方法 addFavorite() 是將一個(gè)食譜添加為最愛,我們來看看應(yīng)該怎么寫測試用例。

@Test
fun addFavorite_shouldUpdateRecipeStatus() {
    val recipe = Recipe("id", "title", "imageUrl", "sourceUrl", false)
    presenter.addFavorite(recipe)
    assertThat(recipe.isFavorited, `is`(true))
}

還是很簡單,我們構(gòu)造了一個(gè)默認(rèn) favorited 屬性為 false 的 recipe,然后調(diào)用 addFavorite() 方法,然后去驗(yàn)證 recipe 對(duì)象的 isFavorited 屬性應(yīng)該是 True . 這里驗(yàn)證的時(shí)候使用了 JUnit 庫中的 assertThat() 方法,這個(gè)方法接收兩個(gè)參數(shù) ,第一個(gè)參數(shù)是驗(yàn)證的目標(biāo),第二個(gè)參數(shù)是一個(gè) Matcher,因?yàn)?kotlin 中 is 是保留關(guān)鍵字,所以需要用 ` 進(jìn)行轉(zhuǎn)義。

相似的,也可以給 presenter 的 removeFavorite() 方法添加測試用例。

完整的測試類

好了,現(xiàn)在我們可以給 Presenter 編寫出一個(gè)完整的測試類了,看一下完整的代碼。

class SearchResultsPresenterTests {

    private lateinit var repository: RecipeRepository
    private lateinit var presenter: SearchResultsPresenter
    private lateinit var view: SearchResultsPresenter.View

    @Before
    fun setup() {
        repository = mock()
        view = mock()
        presenter = SearchResultsPresenter(repository)
        presenter.attachView(view)
    }

    @Test
    fun search_callsShowLoading() {
        presenter.search("eggs")
        verify(view).showLoading()
    }

    @Test
    fun search_succeed_callShowRecipes() {
        val recipe = Recipe("id", "title", "imageUrl", "sourceUrl", false)
        val recipes = listOf(recipe)

        doAnswer {
            val callback: RepositoryCallback> = it.getArgument(1)
            callback.onSuccess(recipes)
        }.whenever(repository).getRecipes(eq("eggs"), any())

        presenter.search("eggs")

        verify(repository).getRecipes(eq("eggs"), any())
        verify(view).showRecipes(eq(recipes))
    }

    @Test
    fun search_error_callShowError() {
        doAnswer {
            val callback: RepositoryCallback> = it.getArgument(1)
            callback.onError()
        }.whenever(repository).getRecipes(eq("eggs"), any())

        presenter.search("eggs")

        verify(repository).getRecipes(eq("eggs"), any())
        verify(view).showError()
    }

    @Test
    fun addFavorite_shouldUpdateRecipeStatus() {
        val recipe = Recipe("id", "title", "imageUrl", "sourceUrl", false)
        presenter.addFavorite(recipe)
        assertThat(recipe.isFavorited, `is`(true))
    }

    @Test
    fun removeFavorite_shouldUpdateRecipeStatus() {
        val recipe = Recipe("id", "title", "imageUrl", "sourceUrl", true)
        presenter.removeFavorite(recipe)
        assertThat(recipe.isFavorited, `is`(false))
    }
}

這已經(jīng)是一個(gè)相對(duì)完整的測試類了,在類聲明的第一行的左邊,同樣有一個(gè)按鈕點(diǎn)擊后可以運(yùn)行整個(gè)類內(nèi)定義的所有測試用例,同樣也有快捷鍵 Ctrl + Shift + R,光標(biāo)放到類上運(yùn)行即可。執(zhí)行結(jié)果如下圖。

如何判斷測試的有效性

測試代碼很快寫完了,你可能會(huì)想,怎么才能衡量測試的有效性呢?這里就要引入另外一個(gè)概念,叫測試覆蓋率 (Code Coverage)。

測試覆蓋率有著不同的維度,比如類數(shù)量、方法數(shù)量、行數(shù)、條件分支等等,具體什么意思不在本文討論范圍,大家可以自行探索。Android Studio 內(nèi)置了工具可以幫我們進(jìn)行統(tǒng)計(jì)。

回顧前面運(yùn)行測試用例的時(shí)候,Android Studio 會(huì)幫我們創(chuàng)建一個(gè) Task,而在運(yùn)行按鈕右邊,還有一個(gè)按鈕叫 “Run [test-task-name] with coverage”,這個(gè)就是 IDE 內(nèi)置的統(tǒng)計(jì)測試覆蓋率的工具啦。

運(yùn)行之后會(huì)自動(dòng)打開一個(gè) Coverage 結(jié)果頁面窗口,點(diǎn)進(jìn)去就可看到當(dāng)前測試 task 對(duì)相關(guān)的被測試代碼的一個(gè)覆蓋情況。結(jié)果顯示我們的測試用例覆蓋了 100% 的類和方法和 88% 的行數(shù)。

點(diǎn)擊打開具體類還能看到每一行代碼有沒有執(zhí)行到,非常好用,為我們對(duì)測試用例的調(diào)整和完善提供了很好的參考價(jià)值。比如,觀察這個(gè) addFavorite() 方法,我們的測試用例沒有覆蓋到 view 的 refresh 方法調(diào)用情況。

陷阱注意!

看起來測試覆蓋率是一個(gè)很好的衡量單元測試覆蓋程度甚至是質(zhì)量的指標(biāo)注意,實(shí)際上確實(shí)有很多人也因此會(huì)追求 100% 的測試覆蓋率,但這樣真的好嗎?

“單元測試并不是越多越好,而是越有效越好?!边@句話不是我說的,而是 Kent Beck 說的,他是 TDD 和 XP 的發(fā)起者,也是敏捷開發(fā)的奠基人。說這些的意思是提醒大家不要陷入教條主義,測試的目的是為了提升對(duì)代碼質(zhì)量,只要自己和團(tuán)隊(duì)有信心,就愛怎么測試就怎么測,怎么合適怎么測,沒有必要一定要寫測試,一定要測試先行。

延伸閱讀

OK,到此為止,你應(yīng)該已經(jīng)學(xué)會(huì)了編寫 Android 單元測試的基本知識(shí),如果想進(jìn)一步了解 Android 測試,建議可以閱讀以下資料:

如果相對(duì) Android 測試有系統(tǒng)的了解,可以參考 Google 官方文檔developer.android.com | testing fundamentals

如果你想理解 Android UI 測試,可以參考 Google 在 19 年 IO 上剛剛更新的 codeLab Android Testing Codelab

查看 Mockito-Kotlin 和 Mockito 的 wiki 來了解更多與 Mock 相關(guān)的知識(shí)

CoolShell 上有一篇文章討論了 “單元測試要做多細(xì)?” | 酷 殼 - CoolShell

Happy unit testing!

參考

android unit testing with mockito | raywenderlich.com

“單元測試要做多細(xì)?” | 酷 殼 - CoolShell

The Way of Testivus - Unit Testing Wisdom From An Ancient Software Start-up

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/7051.html

相關(guān)文章

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<