今天要講一個非常有幫助的主題,現實的世界裡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顯示邏輯有沒有正確那我們就必須用假資料,用假資料有以下幾種做法
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()
}
}
}
}
}
要用上面的第三種方法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)的問題。