Кotlin Coroutines стали популярним інструментом для роботи з асинхронним кодом в мові програмування Kotlin. Вони дозволяють зручно та ефективно працювати з паралельним виконанням, уникнути блокування потоків і забезпечити зручний синтаксис. Однак, при розробці програм з використанням корутин виникають ситуації, коли потрібно перевірити, які саме диспетчери використовуються у ваших асинхронних операціях, зокрема, під час юніт-тестування. У цій статті ми розглянемо, як правильно перевіряти диспетчери корутин під час юніт-тестування на прикладі Kotlin-класу, який приймає диспетчер як параметр.
Спочатку давайте зрозуміємо, що таке диспетчер корутин. Диспетчер – це механізм, який визначає, на якому потоці або потоках виконується корутинна робота. Kotlin надає кілька вбудованих диспетчерів, таких як Dispatchers.Main
, Dispatchers.IO
, Dispatchers.Default
та інші. Ви також можете створювати власні диспетчери за допомогою класу CoroutineDispatcher
.
У багатьох випадках ви використовуватимете деякий диспетчер у вашому коді, і у нього буде важливе значення під час тестування. Наприклад, ви можете мати клас, який приймає диспетчер як параметр у конструкторі та використовує його для виконання асинхронних операцій:
1 2 3 4 5 6 7 8 |
class SomeClass( private val dispatcher: CoroutineDispatcher, private val someRepository: SomeRepository ) { suspend fun fetchData(): SomeData = withContext(dispatcher) { someRepository.fetchData() } } |
Тепер, якщо ви хочете написати юніт-тест для цього класу, де ви хочете перевірити, що fetchData()
викликається з відповідним диспетчером, вам потрібно буде знати, який саме диспетчер використовується у вашому тесті.
Давайте розглянемо практичний приклад. Уявімо, що у нас є клас Test
, який приймає диспетчер як параметр та використовує його для виконання асинхронних операцій:
1 2 3 4 5 6 7 8 9 10 |
class Test( private val dispatcher: CoroutineDispatcher, private val foo: Foo // Just another arg ) { suspend fun testMethod(): Int = withContext(dispatcher) { // Some asynchronous operations foo.doSomething() return@withContext 42 } } |
Для того щоб протестувати цей клас, ми можемо передати власний диспетчер під час створення екземпляру класу Test
у нашому тесті:
1 2 3 4 5 |
val mainTestDispatcher = TestCoroutineDispatcher() val testSubject = Test( dispatcher = mainTestDispatcher, foo = mockk() // Using MockK for mocks ) |
Тепер ми хочемо переконатися, що метод doSomething()
викликається з відповідним диспетчером. Для цього ми можемо використати функцію coAnswers
з бібліотеки MockK, яка дозволяє нам визначити поведінку мока в асинхронному контексті. У нашому тесті ми перевіряємо, що контекст відповідності диспетчера дорівнює тому, який ми передали під час створення класу:
1 2 3 4 5 6 7 8 9 10 |
@Test fun test() { every { foo.doSomething() } coAnswers { val dispatcher = coroutineContext[ContinuationInterceptor]!! dispatcher shouldBe mainTestDispatcher } runBlocking { testSubject.testMethod() } } |
У цьому тесті ми використовуємо функцію every
з MockK для визначення поведінки мока, який відповідає на виклик doSomething()