Тестирование сопрограмм в Котлине - PullRequest
1 голос
/ 02 апреля 2019

У меня есть простой тест на сканер, который должен вызывать репо 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 ]

2 голосов
/ 04 апреля 2019

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

2 голосов
/ 02 апреля 2019

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 не возвращает никакого значения, поэтому вместо него лучше использовать запуск:

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

runBlocking {
    concurrentCrawl().joinAll()
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...