【问题标题】:Testing coroutines in Kotlin在 Kotlin 中测试协程
【发布时间】:2019-08-23 04:43:23
【问题描述】:

我有一个关于应该调用 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一直在变化;所以看起来测试没有等待......)

为什么我睡觉时它会过去?考虑到我运行阻塞,它不应该是必需的..

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

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

谢谢

【问题讨论】:

    标签: kotlin junit kotlin-coroutines


    【解决方案1】:

    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()
    }
    

    如 cmets 中所述:在这种情况下,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()
    }
    

    【讨论】:

    • 谢谢!但这不会让concurrentCall 被阻塞吗?或者是线程上下文中的awaitAll
    • 是的,这会使concurrentCrawl 阻塞,但各个爬网将并行执行。如果您想等待作业在concurrentCrawl 之外继续,您必须将作业传递给调用函数。我在回答中包含了一个示例
    • 需要suspend 吗?我从文档中不确定何时应该使用它。谢谢!
    • 在第一种情况下,awaitAll 挂起,所以concurrentCrawl 必须是suspend fun。在第二种情况下,它永远不会挂起,因此它不应该是suspend fun。另外,当您没有从协程获得结果值时,不要使用async。基本上,Deferred&lt;Unit&gt; 本身就是一种代码味道。
    • 所以..暂停意味着它可以并行运行或暂停而不阻塞主流对吗?关于Deferred&lt;Unit&gt;,我想你的意思是我应该改用launch
    【解决方案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 秒之前的任何时间点成功,它将通过并继续。否则,验证(和测试)将失败。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-07-05
      • 1970-01-01
      • 2019-11-10
      • 1970-01-01
      • 1970-01-01
      • 2020-04-24
      • 2020-09-18
      相关资源
      最近更新 更多