iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 20
0

今天要講一個非常有幫助的主題,現實的世界裡Client跟Server的溝通是免不了的事情,除非你的Applcation只是本機端的小工具之類的程式,不然你或是需要儲存一些資料在遠端永久保存,至少有user data是很常見的事。

假設我們的MainActivity現在要對ServerHelper發一個reqeust把一個字串拿回來顯示在TextView裡面,我們在做整合測試的時候不想要真的去連線server那要怎麼做?

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ServerHelper().request { response ->
            textView.text = response
        }
    }

我們要發request的url為google首頁。在ServerHelper類別裡我們利用Okhttp3的library來協助我們做連線的動作,所以要加入下面的dependency。

    implementation 'com.squareup.okhttp3:okhttp:4.1.0'

在ServerHelper的request function裡,我們利用lambda方式傳入一個callback的function用來接收透過okhttp3發出request的response的字串。okhttp3實作的細節就不詳述,因為不是測試的重點。最後在callback的地方會把response的string post到UI thread給MainActivity的textView來做顯示,當然連線到google首頁的html是一堆字串。

如果今天在做整合測試的時候你不想真的去連production server,只想測試UI顯示邏輯有沒有正確那我們就必須用假資料,用假資料有以下幾種做法

  • 連Testing server,先放測試資料在上面
    • 這種做法比較麻煩的是server上的資料必須有setup和remove的程序,假設你建立的一個假user如果你沒清掉下次再跑測試的時候這個user已經被建立過了就會有conflict失敗的情況,
    • 跟Testing server連線失敗的話測試就會fail但測試本身的邏輯可能是對的。
    • 跟Testing server連線的部份可以放在End to End Test再來一起做。
  • 取代request Server的程式碼,跳過request流程
    • 你的production程式碼可能需要改動,而且無法驗證request邏輯被呼叫的流程
  • 用Local server
    • 在local建立暫時的server,不修改production程式碼即可驗證request flow也不必建立及刪除remote server data,我們的介紹就是Local server部份。
object Constant {
    val url = "http://google.com"
}

class ServerHelper() {
    fun request(callback: (response: String) -> Unit) {
        HandlerThread("demo").apply {
            start()
            Handler(looper).post {
                val client = OkHttpClient()
                val request = Request.Builder()
                    .url(Constant.url)
                    .build()
                try {
                    val response = client.newCall(request).execute()
                    val responseString = response.body?.string()
                    responseString?.let { text ->
                        Handler(Looper.getMainLooper()).post { callback(text) }
                    }
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
        }
    }
}

MockWebServer Library

要用上面的第三種方法Local Server我們可以透過MockWebServer的幫助。

我們要在Instrumentation Test使用Mockk及MockWebServer的話在build.gradle要加入下面的dependencies

     androidTestImplementation("io.mockk:mockk-android:1.9.3.kotlin12") { exclude module: 'objenesis' }
    androidTestImplementation 'org.objenesis:objenesis:2.6'
    androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.1.0'

都設置好後我們就進入測試的部份,這裡我們會利用MockWebServer()建立一個暫時的local server,然後把Server的真實連線用這個local server取代。

而在這個local server response的部份,我們可以用MockResponse()來建立一個response給這個local server,當測試程式跟local server發起request的時候就把這一個MockResponse()回傳給測試程式,進而達成我們測試的目的。

1.要特別注意的是MockWebServer的呼叫時間要放在launchActivity之前,進入Activity之後在test function呼叫會太慢。
2.Mockk用在Instrumentation Test的時候需要使用Android 9以上的機器。

class ExampleInstrumentedTest {
    //我們預設跟server reqeust後會回傳的字串
    private val mockedResponse = "I am mocked response"

    @get:Rule
    val activityTestRule: ActivityTestRule<MainActivity> = object : ActivityTestRule<MainActivity>(MainActivity::class.java) {
        override fun launchActivity(startIntent: Intent?): MainActivity {
            //呼叫MockWebServer的實體並預先給一個假的response
            val server = MockWebServer()
            server.enqueue(MockResponse().apply {
                setResponseCode(200)
                addHeader("Content-Type", "application/json;charset=utf-8")
                addHeader("Cache-Control", "no-cache")
                //在setBody時給假資料
                setBody(mockedResponse)
            })
            //mocked server的url
            val url = server.url("").toString()

            //介由mockk的幫助,當程式呼叫Constant.url把它換成MockWebServer的URL
            mockkObject(Constant)
            every { Constant.url }.returns(url)

            return super.launchActivity(startIntent)
        }
    }

       
   @Test
    fun testMockServer() {
      //MainActivity被launch後跟server連線時被導到MockWebServer我們預設的response
      //"I am mocked response"
      //因此textView會顯示到我們預設從MockWebServer來的I am mocked response,所以測試通過
      onView(withId(R.id.textView)).check(matches(withText(mockedResponse)))
    }
}

藉由MockWebServer()這個Library的協助,我們可以不修改production code就可以測試需要連線到server的test case是不是很方便,可以讓我們只專注在我們UI邏輯部份而不用去關注server的testing data存在問題。

我們這章節有提到有一個方法是連到testing server去測試跟server有關的邏輯,下一節會提到利用Espresso要怎麼去克服網路連線非同步(Asynchronous callback)的問題。


上一篇
[Day 19] Android Espresso 測試客制化UI元件
下一篇
[Day 21] Android Espresso 處理非同步呼叫
系列文
從0開始,全方面自動化測試Android App30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言