iT邦幫忙

2021 iThome 鐵人賽

DAY 27
0
Software Development

Coroutine 停看聽系列 第 27

Day27:測試 Coroutine

Coroutine 是非同步程式的解決方案,我們將耗時的任務置放在 suspend 函式中,在正常的使用 coroutine 情況之下,這些 suspend 函式需要在 coroutine scope 中執行,而建立一個 coroutine scope 的方式有 launchasync

在前面的文章中,我們的程式都是直接跑在 main 函式,並且用 runBlocking 來建立一個 coroutine scope,runBlocking 是一個建立 coroutine 的方法,但是跟 launch 以及 async 不同的是,它可以在 main 函式上直接使用,所以它等於是一般的函式與 coroutine 之間的橋樑,另外,在 runBlocking 中的內容是會佔用目前的執行緒,使用 runBlocking 之後,我們才能夠在 runBlocking 裏面呼叫 launch 以及 async。

那... 這個跟測試有什麼關係呢?

suspend 函式必須要放在一個 coroutine 中,如前面所說的我們用 runBlocking 建立一個 coroutine,然後我們才能使用 suspend 函式,或是使用 launch 、 async 來建立新的 coroutine。如果我們直接在測試程式碼中,直接使用 runBlocking 來測試我們的 suspend 函式,那麼如果需要耗費比較多時間的函式,那就會真的需要耗費那麼長的時間。

kotlinx-coroutines-test

Coroutine 提供了一個供測試用的函式庫,其中最重要的方法就是 runBlockingTest ,在 runBlockingTest 裏面,提供了時間加速的功能,可以減少我們所等待的時間。

dependency

dependencies {
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
}

例如,有一個 suspend 函式,使用 withTimeout 限制呼叫的時間。

class Day27 {
    suspend fun download(service: IService): String {
        withTimeout(1000) {
            return@withTimeout service.download()
        }
        return "fail"
    }
}

我們在這個函式 download 中,將 Service 由外部注入,那麼我們就可以在之後的測試進行我們的修改。

其中 IService 是一個介面,裡面只有一個 suspend 函式 download

interface IService {
    suspend fun download(): String
}

我們在測試的程式碼中,想要測試兩個部分,一個是沒有超時,另一個則是超時的情況。

  • 正常的情況(無超時)
internal class Day27Test {

    private lateinit var day27: Day27

    @BeforeEach
    internal fun setUp() {
        day27 = Day27()
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    internal fun downloadTest() = runBlockingTest {
        kotlin.test.assertEquals("done", day27.download(FakeService(100)))
    }
}

Imgur

其中 FakeService 如下

class FakeService(private val timeout: Long) : IService {
    override suspend fun download(): String {
        delay(timeout)
        return "done"
    }
}

其中 FakeService 如下

class FakeService(private val timeout: Long) : IService {
    override suspend fun download(): String {
        delay(timeout)
        return "done"
    }
}
  • 超時的版本如下:
	@OptIn(ExperimentalCoroutinesApi::class)
  @Test
  internal fun downloadTest_Fail() = runBlockingTest {
      var isException = false
      try {
          day27.download(FakeService(1500))
      } catch (e: TimeoutCancellationException) {
          isException = true
      }
      assert(isException)
  }

Imgur

原本需要花費 1500 毫秒的測試,利用 runBlockingTest 將 suspend 函式包在內後,時間就被加速了,直接走到 timeout,並且只花了 50 毫秒,以這次的結果來看,甚至比正常的情況還要快。

這就是 runBlockingTest 的用處。

小結

本篇文章簡單介紹了 Coroutine 測試函式庫 kotlinx-coroutines-test ,在這裡面包含了 runBlockintTest ,我們將 suspend 函式放進 runBlockintTest 的區塊中就可以進行測試。

除了可以將 suspend 函式帶入外,它還有另外一個功能,那就是時間加速 (advance time)。在 runBlockingTest 的區塊中,如果有 suspend 函式,那麼它就會加速它直到遇到 timeout。

kotlinx-coroutines-test 目前還有很多項目都被標為 ExperimentalCoroutinesApi ,在正式版出來之前,這些內容都有可能會更改,使用上要特別注意。

參考資料

kotlinx.coroutines/kotlinx-coroutines-test at master · Kotlin/kotlinx.coroutines

KotlinConf 2019: Testing with Coroutines by Sean McQuillan

特別感謝

Kotlin Taiwan User Group

Kotlin 讀書會


上一篇
Day26:Flow 的運算子 - buffer()
下一篇
Day28:複習 Coroutine
系列文
Coroutine 停看聽30

尚未有邦友留言

立即登入留言