iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 4
0
Mobile Development

從0開始,全方面自動化測試Android App系列 第 4

[Day 4] 從MVP模式開始練習Unit test

MVP (Model-View-Presenter)

MVP是在Mobile Application常使用的設計架構,它清楚的把邏輯跟UI元件的相互關係分開來呈現,也有利於單元測試的進行,如果你不知道怎麼架構可被單元測試的程式,那MVP就是很好的入門心法。

Model

Model跟View在我們原本的程式內就有設計的東西,Model可以定義成我們資料的架構,一個application裡面一定有資料需要在不同的地方流動傳遞,例如User data 登入後我們會拿User data裡面的username來顯示 因此我們把資料定義成Model。

data class User(val username: String, val age: Int, val gender: String)

View

而View就是我們在Application裡面控制顯示行為的地方,在Android的架構裡就是Activity或Fragment等(下文都以Activity為例),Activity裡面會把layout.xml檔案定義的UI元件在runtime時期render出來,而每個UI元件就是一個resource,我們會把相對應的UI狀態放入每個resource裡面。例如是我們操作TextView的行為就是View的範疇。

textView.text = "Daniel Chen"

Presenter

Presenter照字面翻譯就是對View的表達或是表示,用來把一些不會直接呼叫UI的邏輯程式放在Presenter裡面,等到有需要的時候再通知給View知道,讓Activity做相對應的行為。

剛剛提過每個UI元件所產生的resource在unit test的架構裡面是無法被測試的東西,UI的部分是要launch application process起來才可以被測試,所以我們在Presenter裡必須就完全不引用UI元件的reference,只利能利用View的interface來跟View溝通。我想第一次接觸的人一定聽的一頭霧水,等一下會直接用程式碼舉例會比較容易了解。

假設我們想把昨天提到在預設的MainActivity範例中TextView的Hello World!字串做一個測試,雖然我們無法直接去測試View的部份(TextView),但我們可以利用MVP設計模式來測試Presenter跟View溝通流程的部份是否如預期一樣,有把字串從Presenter傳到Activity去。

接下來我們把code改一下,假設我們現在進入MainActivity的時候要跟Server要username叫做Daniel Chen回來MainActivity的TextView顯示如下圖。
https://ithelp.ithome.com.tw/upload/images/20190919/20120975thvkeBjgK0.png

因為跟Server request的資料處理部份很明顯跟UI元件沒直接關係,因此我們可以準備一個Presenter class來包裝Server request的部份,這邊我寫了一個MainActivityPresenter來包裝Server reqeust。

//Model
data class User(var firstName: String, var lastName: String)

class Server {
    fun requestUserName(): User {
        return User("Danie", "Chen")
    }
}

//Presenter
class MainActivityPresenter(private val view: IView) {
    
    fun requestUserName() {
        val user = Server().requestUserName()
        view.receivedUserName("${user.firstName} ${user.lastName}")
    }
}

上面寫了一個reqeustUserName()的函式來示範我們跟Server發出的reqeust,裡面很簡單的只return一個Daniel Chen的字串。在View部份我們有說過Activity是無法被單元測試,所以我們用一個interface來abstract(抽象)View的行為,等一下我們只需針對抽象的部份來個測試就不用真的去測試UI需要runtime resource的部份。

//View
interface IView {
    fun receivedUserName(name: String)
}

針對這個需求我們創建一個IView的interface,裡面建立Presenter需要告訴Activity的資訊的抽象函式receivedUserName(name: String),而username的組成就是由User data model裡去獲得。
這時候可以看出一個Model-View-Presenter一個簡單的模型出現了。

class MainActivity : AppCompatActivity(), IView {

    private var presenter = MainActivityPresenter(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        presenter.requestUserName()
    }

    override fun receivedUserName(name: String) {
        textView.text = name
    }
}

最後我們把MainActivity去實作IView這個interface,MainActivity就具備receivedUserName這個行為,我們這時候把真的要setText的這個UI行為放到這裡來做,一旦Presenter完成一個邏輯就callback一個抽象行為來MainActivity實現,而不在Presenter裡面實現。這樣Presenter就可以保持一個很乾淨可以被測試的邏輯,當然前提是你別又忍不住把View的resource放到Presenter裡來使用,因為剛開始學習MVP的人很容易便宜行事直接把UI元件的reference放在presneter裡使用,這樣等於就破壞了MVP的理念了。

單元測試開始

完成MVP的設計模式後我們就可以來進行單元測試了,所謂單元測試就是針對程式內的function去做測試,我們已經把非UI的邏輯放在Presenter了,因此我們就針對Presenter做測試。在Java或Kotlin裡做Unit test除了基本的JUnit提供我們測試環境外,如果我們須要用到自定類別我們常常須要去mock object,所以還需要其它mocking framework的協助。比較有名的是Mockito,但是Mockito用來測試Android會有點力不從心,因為Android本身使用許多singleton及static類別,但是在Mockito裡面並不支持這樣的做法,因為這樣會違反OOP設計的原則,所以我們在Android裡使用必須額外搭配的Library如PowerMock等library,但實際使用起來常常會遇到無法處理的例外狀況。我推薦Mockk這個專門為Kotlin + Android打造的library,使得使用起來在許多在Mockito會遇到的例外狀況迎刃而解,只要在build.gradle裡加下面的dependency即可。

dependencies {
    testImplementation "io.mockk:mockk:1.9.3.kotlin12"
}

在進行單元測試開始前我們先要定義Test Case,因為我們的範例在Presenter裡只有一個function叫reqeustUserName
,因此我們的目標就是reqeustUserName(),知道要測試的function後,我們下一步要定義要驗證什麼?在requestUserName()裡最後會呼叫view.receivedUserName(),也就是我們要驗證receivedUserName()有被呼叫到,當receivedUserName()有被呼叫也就等於Activity會收到這個通知去做UI的相對應行為。

這個Test case (testRequestUserName)定義如下

  • 要測試的是 -> MainActivityPresenter.requestUserName()
  • 要驗證的是 -> IView.receivedUserName()
    @Test
    fun testRequestUserName() {
        val view = mockk<IView>() //假造一個IView的空實體
        val presenter = MainActivityPresenter(view) //我們實際測試的部份

     //當假造的IView被呼叫到receivedUserName(),就讓它執行下去runs。
        every {
            view.receivedUserName(any()) 
        } just Runs

        //我們針對實際測試的部份(presenter)呼叫測試function
        presenter.requestUserName()

        //驗證IView在presenter.requestUserName()後是否有被呼叫receivedUserName()
        verify {
            view.receivedUserName(any())
        }
    }

我們的測試程式如上,一樣在ExampleUnitTest class裡新增一個測試function叫testRequestUserName(),首先我們依照我們剛剛定義好的架構來寫,presenter是我們要測試的目標,所以我們把它實體化。而IView介面所定義的receivedUserName()則是我們要驗證的部份,所以我們用一個mockk定義的verify方法來驗證receivedUserName是否有被呼叫。那在開始執行presenter.requestUserName()前我們需要針對執行requestUserName()這個function裡面有用到的dependency object給一些假設條件,因為我們不是跑真的Android流程,在正式流程中presenter的IView實體是由Activity來給的,所以我們在單元測試中要執行presetner的話也要動態傳入給它,不然它就變成Null object了,而在這裡是用mockk的方式來把它實體化再傳給Presenter。

這個測試最後的目的是我們可以藉由receivedUserName是否有被呼叫這件事來驗證Daniel Chen這個字串是否有被傳到Activity裡,至於在Activity裡的receivedUserName()中UI實作細節在單元測試中並不關心,因為老話一句UI行為無法測試。

在下一個章節我會繼續介紹在單元測試中Mock的觀念跟Mockk一些常用的語法。因為Mock是單元測試中最基本也是最重要的觀念。


上一篇
[Day 3]用JUnit環境練習第一個Unit test
下一篇
[Day 5] DIY寫一個Mock object
系列文
從0開始,全方面自動化測試Android App30

尚未有邦友留言

立即登入留言