#kotlin #junit #kotlin-coroutines
#kotlin #junit #kotlin-сопрограммы
Вопрос:
У меня есть этот простой тест для сканера, который должен вызывать репозиторий 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
}
Спасибо
Ответ №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()
}
Как упоминалось в комментариях: В этом случае 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. Спасибо! Но не приведет ли это к
concurrentCall
блокировке? Или этоawaitAll
в контексте потоков?2. Да, это приводит к
concurrentCrawl
блокировке, но отдельные обходы будут выполняться параллельно. Если вы хотите дождаться продолжения заданий за пределамиconcurrentCrawl
, вы должны передать задание вызывающей функции. Я включил пример для этого в свой ответ3. было ли
suspend
необходимо? Я не уверен из документов, когда это следует использовать. Спасибо!4. В первом случае
awaitAll
приостанавливается, поэтомуconcurrentCrawl
должно бытьsuspend fun
. Во втором случае он никогда не приостанавливается, поэтому он не должен бытьsuspend fun
. Кроме того, не используйтеasync
, когда вы не получаете результирующее значение из сопрограммы. По сути,Deferred<Unit>
это собственный запах кода.5. Итак .. приостановка означает, что он может работать параллельно или приостановлено, не блокируя основной поток, верно? Что касается
Deferred<Unit>
, я полагаю, вы имеете в виду, что я должен использовать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 секунд, она пройдет и будет продолжена. В противном случае проверка (и тест) завершатся неудачей.