在Kotlin中测试协程

时间:2019-04-01 23:25:05

标签: kotlin junit kotlin-coroutines

我有一个关于爬虫的简单测试,该爬虫应该调用回购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一直在变化;因此似乎测试没有在等待...)

为什么我睡觉时会过去?鉴于我遇到了阻塞,因此不需要。

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

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

谢谢

2 个答案:

答案 0 :(得分:2)

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结果传递给调用函数,如以下示例所示。在这种情况下,可以从suspend中删除concurrentCrawl()关键字。

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方法不返回任何值,因此最好使用launch代替:

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

runBlocking {
    concurrentCrawl().joinAll()
}

答案 1 :(得分:2)

您也可以为此使用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秒钟之前的任何时间验证成功,它将通过并继续。否则,验证(和测试)将失败。