在 Kotlin 中测试协程

Testing coroutines in Kotlin

我有一个关于应该调用 repo 40 次的爬虫的简单测试:

@Test
fun testX() {
   // ... 
   runBlocking {
        crawlYelp.concurrentCrawl()
        // Thread.sleep(5000) // works if I un-comment
   }
   verify(restaurantsRepository, times(40)).saveAll(restaurants)
   // ...
}

和这个实现:

suspend fun concurrentCrawl() {
    cities.map { loc ->
        1.rangeTo(10).map { start ->
            GlobalScope.async {
                val rests = scrapYelp.scrap(loc, start * 10)
                restaurantsRepository.saveAll(rests)
            }
        }
    }
}

但是...我明白了:

Wanted 40 times:
-> at ....testConcurrentCrawl(CrawlYelpTest.kt:46)
But was 30 times:

(30一直在变,看来考试不等了...)

为什么我做睡眠的时候就过去了?鉴于我 运行 blocking..

不需要它

顺便说一句,我有一个应该保持异步的控制器:

@PostMapping("crawl")
suspend fun crawl(): String {
    crawlYelp.concurrentCrawl()
    return "crawling" // this is supposed to be returned right away
}

谢谢

runBlocking 等待所有挂起函数完成,但由于 concurrentCrawl 基本上只是在 GlobalScope.async currentCrawl 的新线程中启动新作业,因此 runBlocking,在所有作业开始后完成,而不是在所有这些作业完成后完成。

您必须像这样等待以 GlobalScope.async 开始的所有作业完成:

suspend fun concurrentCrawl() {
    cities.map { loc ->
        1.rangeTo(10).map { start ->
            GlobalScope.async {
                val rests = scrapYelp.scrap(loc, start * 10)
                restaurantsRepository.saveAll(rests)
            }
        }.awaitAll()
    }
}

如果您想等待 concurrentCrawl()concurrentCrawl() 之外完成,那么您必须将 Deferred 结果传递给调用函数,如下例所示。在这种情况下,可以从 concurrentCrawl().

中删除 suspend 关键字
fun concurrentCrawl(): List<Deferred<Unit>> {
    return cities.map { loc ->
        1.rangeTo(10).map { start ->
            GlobalScope.async {
                println("hallo world $start")
            }
        }
    }.flatten()
}


runBlocking {
    concurrentCrawl().awaitAll()
}

如评论中所述:在这种情况下,async 方法没有 return 任何值,因此最好改用 launch:

fun concurrentCrawl(): List<Job> {
    return cities.map { loc ->
        1.rangeTo(10).map { start ->
            GlobalScope.launch {
                println("hallo world $start")
            }
        }
    }.flatten()
}

runBlocking {
    concurrentCrawl().joinAll()
}

您也可以为此使用 MockK(以及更多)。

MockK 的 verify 有一个 timeout : Long 参数专门用于在测试中处理这些竞争。

您可以保持生产代码不变,并将您的测试更改为:

import io.mockk.verify

@Test
fun `test X`() = runBlocking {
   // ... 

   crawlYelp.concurrentCrawl()

   verify(exactly = 40, timeout = 5000L) {
      restaurantsRepository.saveAll(restaurants)
   }
   // ...
}

如果在 5 秒之前的任何时候验证成功,它将通过并继续。否则,验证(和测试)将失败。