Тестирование сопрограмм в Kotlin

#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 секунд, она пройдет и будет продолжена. В противном случае проверка (и тест) завершатся неудачей.