کد تست واحد که از کوروتینها استفاده میکند به توجه بیشتری نیاز دارد، زیرا اجرای آنها میتواند ناهمزمان باشد و در چندین رشته اتفاق بیفتد. این راهنما نحوه آزمایش توابع تعلیق، ساختارهای آزمایشی که باید با آنها آشنا باشید و چگونه کد خود را که از کوروتینها استفاده میکند قابل آزمایش کنید، پوشش میدهد.
API های استفاده شده در این راهنما بخشی از کتابخانه kotlinx.coroutines.test هستند. مطمئن شوید که مصنوع را به عنوان یک وابستگی آزمایشی به پروژه خود اضافه کنید تا به این APIها دسترسی داشته باشید.
dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}
فراخوانی توابع تعلیق در تست ها
برای فراخوانی توابع تعلیق در تستها، باید در یک برنامهنویسی باشید. از آنجایی که توابع تست JUnit به خودی خود توابع را تعلیق نمی کنند، برای شروع یک کوروتین جدید باید با یک سازنده کوروتین در داخل تست های خود تماس بگیرید.
runTest
یک سازنده کوروتین است که برای آزمایش طراحی شده است. از این برای بسته بندی هر آزمایشی که شامل کوروتین است استفاده کنید. توجه داشته باشید که کوروتینها نه تنها مستقیماً در بدنه آزمون، بلکه توسط اشیایی که در آزمون استفاده میشوند نیز میتوانند شروع شوند.
suspend fun fetchData(): String { delay(1000L) return "Hello world" } @Test fun dataShouldBeHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) }
به طور کلی، شما باید یک فراخوانی از runTest
در هر آزمون داشته باشید، و استفاده از بدنه عبارت توصیه می شود.
قرار دادن کد تست خود در runTest
برای آزمایش عملکردهای تعلیق اولیه کار می کند و به طور خودکار از هرگونه تأخیر در برنامه ها صرف نظر می کند و باعث می شود که تست فوق سریعتر از یک ثانیه کامل شود.
با این حال، بسته به آنچه در کد تحت آزمایش شما اتفاق می افتد، ملاحظات دیگری نیز وجود دارد:
- زمانی که کد شما برنامههای مشترک جدیدی به غیر از برنامه آزمایشی سطح بالایی که
runTest
ایجاد میکند ایجاد میکند، باید با انتخابTestDispatcher
مناسب، نحوه برنامهریزی آن کوروتینهای جدید را کنترل کنید. - اگر کد شما اجرای اصلی را به توزیعکنندههای دیگر منتقل کند (مثلاً با استفاده از
withContext
)،runTest
همچنان کار میکند، اما تاخیرها دیگر نادیده گرفته نمیشوند و تستها کمتر قابل پیشبینی خواهند بود، زیرا کد روی رشتههای مختلف اجرا میشود. به این دلایل، در آزمایشها باید دیسپاچرهای آزمایشی را برای جایگزینی دیسپاچرهای واقعی تزریق کنید .
TestDispatchers
TestDispatchers
پیاده سازی CoroutineDispatcher
برای اهداف آزمایشی هستند. در صورتی که در حین آزمایش کوروتین های جدیدی ایجاد شود تا اجرای کوروتین های جدید قابل پیش بینی باشد، باید از TestDispatchers
استفاده کنید.
دو پیادهسازی در دسترس از TestDispatcher
وجود دارد: StandardTestDispatcher
و UnconfinedTestDispatcher
، که برنامهریزیهای مختلفی را برای برنامههای تازه شروع شده انجام میدهند. این هر دو از TestCoroutineScheduler
برای کنترل زمان مجازی و مدیریت برنامههای در حال اجرا در یک آزمون استفاده میکنند.
فقط باید یک نمونه زمانبندی در یک آزمایش استفاده شود که بین همه TestDispatchers
به اشتراک گذاشته شود. برای آشنایی با اشتراکگذاری زمانبندیها، به Injecting TestDispatchers مراجعه کنید.
برای شروع مراحل تست سطح بالا، runTest
یک TestScope
ایجاد می کند که پیاده سازی CoroutineScope
است که همیشه از TestDispatcher
استفاده می کند. اگر مشخص نشده باشد، یک TestScope
به طور پیشفرض یک StandardTestDispatcher
ایجاد میکند و از آن برای اجرای برنامه آزمایشی سطح بالا استفاده میکند.
runTest
برنامهریزیهایی را که در زمانبندی مورد استفاده توسط توزیعکننده TestScope
در صف قرار میگیرند را پیگیری میکند و تا زمانی که کار معلقی روی آن زمانبند وجود داشته باشد، برنمیگردد.
StandardTestDispatcher
هنگامی که برنامههای جدید را در یک StandardTestDispatcher
شروع میکنید، آنها در صفبندی زمانبندی اصلی قرار میگیرند تا هر زمان که رشته آزمایشی برای استفاده آزاد باشد اجرا شوند. برای اجازه دادن به این کوروتین های جدید، باید رشته آزمایشی را رها کنید (آن را برای استفاده از کوروتین های دیگر آزاد کنید). این رفتار صفبندی به شما کنترل دقیقی بر نحوه اجرای کوروتینهای جدید در طول آزمایش میدهد و شبیه زمانبندی کوروتینها در کد تولید است.
اگر رشته آزمایشی هرگز در طول اجرای کوروتین تست سطح بالا به دست نیاید، هر کوروتین جدید فقط پس از انجام مراحل آزمایشی اجرا می شود (اما قبل از اینکه runTest
برگردد):
@Test fun standardTest() = runTest { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails }
راههای مختلفی برای به دست آوردن کوروتین آزمایشی وجود دارد تا اجازه دهید کوروتینهای در صف اجرا شوند. همه این فراخوانیها اجازه میدهند تا کوروتینهای دیگر قبل از بازگشت در رشته آزمایشی اجرا شوند:
-
advanceUntilIdle
: تمام برنامه های دیگر را در زمانبندی اجرا می کند تا زمانی که چیزی در صف باقی نماند. این یک انتخاب پیشفرض خوب برای اجازه دادن به اجرای تمام کوروتینهای معلق است و در اکثر سناریوهای آزمایشی کار خواهد کرد. -
advanceTimeBy
: زمان مجازی را با مقدار مشخصی افزایش می دهد و هر برنامه ای را که برای اجرا قبل از آن نقطه در زمان مجازی برنامه ریزی شده است اجرا می کند. -
runCurrent
: برنامههایی را اجرا میکند که در زمان مجازی فعلی برنامهریزی شدهاند.
برای اصلاح تست قبلی، advanceUntilIdle
میتواند استفاده شود تا به دو برنامه معلق اجازه دهد تا قبل از ادامه ادعا، کار خود را انجام دهند:
@Test fun standardTest() = runTest { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } advanceUntilIdle() // Yields to perform the registrations assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes }
UnconfinedTestDispatcher
هنگامی که روالهای جدید در UnconfinedTestDispatcher
شروع میشوند، مشتاقانه در رشته فعلی شروع میشوند. این بدان معنی است که آنها بلافاصله شروع به دویدن می کنند، بدون اینکه منتظر بازگشت سازنده کوروتین خود باشند. در بسیاری از موارد، این رفتار ارسال منجر به کد آزمایش سادهتر میشود، زیرا نیازی نیست که به صورت دستی رشته آزمایشی را تحویل دهید تا برنامههای جدید اجرا شوند.
با این حال، این رفتار با آنچه در تولید با توزیع کنندگان غیر آزمایشی مشاهده خواهید کرد متفاوت است. اگر تست شما بر روی همزمانی تمرکز دارد، به جای آن از StandardTestDispatcher
استفاده کنید.
برای استفاده از این دیسپچر برای برنامه تست سطح بالا در runTest
به جای نمونه پیش فرض، یک نمونه ایجاد کنید و آن را به عنوان یک پارامتر ارسال کنید. این باعث میشود که برنامههای جدید ایجاد شده در runTest
مشتاقانه اجرا شوند، زیرا توزیع کننده را از TestScope
به ارث میبرند.
@Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes }
در این مثال، فراخوانهای راهاندازی برنامههای جدید خود را مشتاقانه در UnconfinedTestDispatcher
شروع میکنند، به این معنی که هر تماس برای راهاندازی تنها پس از تکمیل ثبتنام بازمیگردد.
به یاد داشته باشید که UnconfinedTestDispatcher
برنامه های جدید را مشتاقانه شروع می کند، اما این بدان معنا نیست که آنها را نیز با اشتیاق کامل اجرا می کند. اگر کوروتین جدید به حالت تعلیق درآید، سایر کوروتین ها اجرا را از سر خواهند گرفت.
به عنوان مثال، برنامه جدید راهاندازی شده در این آزمون، آلیس را ثبت میکند، اما پس از فراخوانی delay
، به حالت تعلیق در میآید. این اجازه می دهد تا برنامه سطح بالا با ادعا ادامه دهد، و آزمایش با شکست مواجه شود زیرا Bob هنوز ثبت نشده است:
@Test fun yieldingTest() = runTest(UnconfinedTestDispatcher()) { val userRepo = UserRepository() launch { userRepo.register("Alice") delay(10L) userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails }
دیسپچرهای تست تزریقی
کد تحت آزمایش ممکن است از توزیع کننده ها برای جابجایی رشته ها (با استفاده از withContext
) یا برای شروع برنامه های جدید استفاده کند. هنگامی که کد بر روی چندین رشته به صورت موازی اجرا می شود، تست ها می توانند پوسته پوسته شوند. ممکن است انجام اظهارات در زمان صحیح یا منتظر ماندن برای تکمیل کارها در صورتی که روی رشتههای پسزمینه اجرا میشوند که کنترلی روی آنها ندارید، دشوار باشد.
در آزمایشها، این توزیعکنندهها را با نمونههایی از TestDispatchers
جایگزین کنید. این چندین مزیت دارد:
- کد بر روی یک رشته آزمایشی اجرا می شود و آزمایش ها را قطعی تر می کند
- می توانید نحوه برنامه ریزی و اجرای برنامه های جدید را کنترل کنید
- TestDispatchers از یک زمانبندی برای زمان مجازی استفاده میکند، که تأخیرها را بهطور خودکار رد میکند و به شما امکان میدهد زمان را به صورت دستی پیش ببرید.
استفاده از تزریق وابستگی برای ارائه توزیع کننده به کلاس های شما، جایگزینی توزیع کننده های واقعی را در تست ها آسان می کند. در این مثالها، ما یک CoroutineDispatcher
را تزریق میکنیم، اما میتوانید نوع گستردهتر CoroutineContext
را نیز تزریق کنید، که به انعطافپذیری بیشتری در طول آزمایشها اجازه میدهد.
برای کلاسهایی که کوروتینها را شروع میکنند، میتوانید به جای توزیعکننده، CoroutineScope
که در بخش Injecting a scope توضیح داده شده است، تزریق کنید.
TestDispatchers
، به طور پیشفرض، زمانی که نمونهسازی میشوند، یک زمانبندی جدید ایجاد میکنند. در داخل runTest
، میتوانید به ویژگی testScheduler
TestScope
دسترسی داشته باشید و آن را به هر TestDispatchers
که به تازگی ایجاد شدهاند ارسال کنید. با این کار درک آنها از زمان مجازی به اشتراک گذاشته میشود و روشهایی مانند advanceUntilIdle
برنامههای روتین را در تمام توزیعکنندههای آزمایشی اجرا میکنند.
در مثال زیر، می توانید یک کلاس Repository
را ببینید که با استفاده از IO
dispatcher در روش initialize
خود، یک coroutine جدید ایجاد می کند و در متد fetchData
خود، فراخوان دهنده را به توزیع کننده IO
سوئیچ می کند:
// Example class demonstrating dispatcher use cases class Repository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { private val scope = CoroutineScope(ioDispatcher) val initialized = AtomicBoolean(false) // A function that starts a new coroutine on the IO dispatcher fun initialize() { scope.launch { initialized.set(true) } } // A suspending function that switches to the IO dispatcher suspend fun fetchData(): String = withContext(ioDispatcher) { require(initialized.get()) { "Repository should be initialized first" } delay(500L) "Hello world" } }
در آزمایشها، میتوانید پیادهسازی TestDispatcher
برای جایگزینی توزیعکننده IO
تزریق کنید.
در مثال زیر، ما یک StandardTestDispatcher
را به مخزن تزریق میکنیم و از advanceUntilIdle
استفاده میکنیم تا مطمئن شویم که coroutine جدید که در initialize
شروع شده است، قبل از ادامه تکمیل میشود.
fetchData
همچنین از اجرای بر روی TestDispatcher
سود میبرد، زیرا روی رشته آزمایشی اجرا میشود و از تأخیر موجود در طول آزمایش رد میشود.
class RepositoryTest { @Test fun repoInitWorksAndDataIsHelloWorld() = runTest { val dispatcher = StandardTestDispatcher(testScheduler) val repository = Repository(dispatcher) repository.initialize() advanceUntilIdle() // Runs the new coroutine assertEquals(true, repository.initialized.get()) val data = repository.fetchData() // No thread switch, delay is skipped assertEquals("Hello world", data) } }
برنامه های جدید شروع شده در TestDispatcher
را می توان به صورت دستی همانطور که در بالا نشان داده شده است با initialize
ارتقا داد. البته توجه داشته باشید که این امر در کد تولید امکان پذیر یا مطلوب نخواهد بود. در عوض، این روش باید به گونهای طراحی شود که به حالت تعلیق (برای اجرای متوالی)، یا برای بازگشت یک مقدار Deferred
(برای اجرای همزمان) باشد.
به عنوان مثال، می توانید از async
برای شروع یک کوروتین جدید و ایجاد Deferred
استفاده کنید:
class BetterRepository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { private val scope = CoroutineScope(ioDispatcher) fun initialize() = scope.async { // ... } }
این به شما امکان می دهد با خیال راحت await
تکمیل این کد در تست و کد تولید باشید:
@Test fun repoInitWorks() = runTest { val dispatcher = StandardTestDispatcher(testScheduler) val repository = BetterRepository(dispatcher) repository.initialize().await() // Suspends until the new coroutine is done assertEquals(true, repository.initialized.get()) // ... }
اگر کوروتینها روی TestDispatcher
باشد که برنامه زمانبندی را با آن به اشتراک میگذارد، runTest
قبل از بازگشت منتظر میماند تا کوروتینهای معلق تکمیل شوند. همچنین منتظر کوروتینهایی میماند که فرزندان مراحل آزمایشی سطح بالا هستند، حتی اگر در توزیعکنندههای دیگر باشند (تا زمانی که توسط پارامتر dispatchTimeoutMs
مشخص شده است که به طور پیشفرض 60 ثانیه است).
تنظیم توزیع کننده اصلی
در آزمایشهای واحد محلی ، توزیعکننده Main
که رشته رابط کاربری Android را میپیچد، در دسترس نخواهد بود، زیرا این آزمایشها بر روی یک JVM محلی و نه یک دستگاه Android اجرا میشوند. اگر کد تحت آزمایش شما به رشته اصلی ارجاع می دهد، در طول تست های واحد یک استثنا ایجاد می کند.
در برخی موارد، میتوانید Main
Dispatcher را مانند سایر Dispatcherها تزریق کنید، همانطور که در بخش قبل توضیح داده شد و به شما امکان میدهد آن را با TestDispatcher
در آزمایشها جایگزین کنید. با این حال، برخی از APIها مانند viewModelScope
از یک توزیع کننده Main
رمزگذاری شده در زیر هود استفاده می کنند.
در اینجا مثالی از اجرای ViewModel
است که از viewModelScope
برای راهاندازی برنامهای که دادهها را بارگیری میکند، استفاده میکند:
class HomeViewModel : ViewModel() { private val _message = MutableStateFlow("") val message: StateFlow<String> get() = _message fun loadMessage() { viewModelScope.launch { _message.value = "Greetings!" } } }
برای جایگزینی Main
Dispatcher با TestDispatcher
در همه موارد، از توابع Dispatchers.setMain
و Dispatchers.resetMain
استفاده کنید.
class HomeViewModelTest { @Test fun settingMainDispatcher() = runTest { val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) try { val viewModel = HomeViewModel() viewModel.loadMessage() // Uses testDispatcher, runs its coroutine eagerly assertEquals("Greetings!", viewModel.message.value) } finally { Dispatchers.resetMain() } } }
اگر توزیع کننده Main
با یک TestDispatcher
جایگزین شده باشد، هر TestDispatchers
جدید ایجاد شده به طور خودکار از زمانبندی از Dispatcher Main
استفاده می کند ، از جمله StandardTestDispatcher
ایجاد شده توسط runTest
اگر توزیع کننده دیگری به آن ارسال نشود.
این باعث می شود اطمینان حاصل شود که تنها یک زمانبندی در طول آزمایش استفاده می شود. برای اینکه این کار کار کند، مطمئن شوید که همه نمونه های دیگر TestDispatcher
را پس از فراخوانی Dispatchers.setMain
ایجاد کرده اید.
یک الگوی رایج برای جلوگیری از تکرار کدی که جایگزین Dispatcher Main
در هر آزمایش می شود، استخراج آن در یک قانون تست JUnit است:
// Reusable JUnit4 TestRule to override the Main dispatcher class MainDispatcherRule( val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() } } class HomeViewModelTestUsingRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun settingMainDispatcher() = runTest { // Uses Main’s scheduler val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } }
اجرای این قانون به طور پیشفرض از یک UnconfinedTestDispatcher
استفاده میکند، اما اگر Dispatcher Main
نباید مشتاقانه در یک کلاس آزمایشی اجرا شود، یک StandardTestDispatcher
میتواند به عنوان یک پارامتر ارسال شود.
هنگامی که به یک نمونه TestDispatcher
در بدنه آزمایشی نیاز دارید، میتوانید از testDispatcher
از قانون استفاده مجدد کنید، تا زمانی که نوع مورد نظر باشد. اگر میخواهید در مورد نوع TestDispatcher
استفاده شده در تست صریح باشید، یا اگر به TestDispatcher
نیاز دارید که نوع متفاوتی از مورد استفاده برای Main
باشد، میتوانید یک TestDispatcher
جدید در runTest
ایجاد کنید. از آنجایی که توزیع کننده Main
روی یک TestDispatcher
تنظیم شده است، هر TestDispatchers
جدید ایجاد شده، زمانبندی خود را به طور خودکار به اشتراک میگذارد.
class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun injectingTestDispatchers() = runTest { // Uses Main’s scheduler // Use the UnconfinedTestDispatcher from the Main dispatcher val unconfinedRepo = Repository(mainDispatcherRule.testDispatcher) // Create a new StandardTestDispatcher (uses Main’s scheduler) val standardRepo = Repository(StandardTestDispatcher()) } }
ایجاد دیسپچرهای خارج از آزمون
در برخی موارد، ممکن است به یک TestDispatcher
نیاز داشته باشید که خارج از روش تست در دسترس باشد. به عنوان مثال، در هنگام مقداردهی اولیه یک ویژگی در کلاس تست:
class ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } class RepositoryTestWithRule { private val repository = ExampleRepository(/* What TestDispatcher? */) @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun someRepositoryTest() = runTest { // Test the repository... // ... } }
اگر همانطور که در بخش قبل نشان داده شده است، توزیع کننده Main
را جایگزین می کنید، TestDispatchers
که پس از جایگزینی توزیع کننده Main
ایجاد شده اند، به طور خودکار زمان بندی آن را به اشتراک می گذارند.
با این حال، این مورد برای TestDispatchers
که بهعنوان ویژگیهای کلاس تست یا TestDispatchers
که در طول اولیهسازی خصوصیات در کلاس تست ایجاد میشوند، صادق نیست. اینها قبل از جایگزینی توزیع کننده Main
مقداردهی اولیه می شوند. بنابراین، آنها برنامهریزیهای جدیدی ایجاد میکنند.
برای اطمینان از اینکه فقط یک زمانبندی در تست شما وجود دارد، ابتدا ویژگی MainDispatcherRule
را ایجاد کنید. سپس در صورت نیاز از توزیع کننده آن (یا زمانبندی آن، در صورت نیاز به TestDispatcher
از نوع متفاوت) در مقداردهی اولیه خصوصیات سطح کلاس دیگر استفاده کنید.
class RepositoryTestWithRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() private val repository = ExampleRepository(mainDispatcherRule.testDispatcher) @Test fun someRepositoryTest() = runTest { // Takes scheduler from Main // Any TestDispatcher created here also takes the scheduler from Main val newTestDispatcher = StandardTestDispatcher() // Test the repository... } }
توجه داشته باشید که هم runTest
و هم TestDispatchers
ایجاد شده در تست همچنان به طور خودکار زمانبندی توزیع کننده Main
را به اشتراک می گذارند.
اگر Dispatcher Main
را جایگزین نمیکنید، اولین TestDispatcher
خود را (که یک زمانبندی جدید ایجاد میکند) به عنوان ویژگی کلاس ایجاد کنید. سپس، آن زمانبند را بهصورت دستی به هر فراخوانی runTest
و هر TestDispatcher
جدید ایجاد شده، هم بهعنوان ویژگی و هم در آزمایش، ارسال کنید:
class RepositoryTest { // Creates the single test scheduler private val testDispatcher = UnconfinedTestDispatcher() private val repository = ExampleRepository(testDispatcher) @Test fun someRepositoryTest() = runTest(testDispatcher.scheduler) { // Take the scheduler from the TestScope val newTestDispatcher = UnconfinedTestDispatcher(this.testScheduler) // Or take the scheduler from the first dispatcher, they’re the same val anotherTestDispatcher = UnconfinedTestDispatcher(testDispatcher.scheduler) // Test the repository... } }
در این نمونه، زمانبندی از اولین توزیع کننده به runTest
منتقل می شود. این یک StandardTestDispatcher
جدید برای TestScope
با استفاده از آن زمانبندی ایجاد می کند. همچنین میتوانید مستقیماً در دیسپاچر به runTest
بفرستید تا برنامه آزمایشی روی آن دیسپچر اجرا شود.
ایجاد TestScope خود
مانند TestDispatchers
، ممکن است لازم باشد به TestScope
خارج از بدنه تست دسترسی داشته باشید. در حالی که runTest
یک TestScope
را به صورت خودکار در زیر هود ایجاد می کند، شما همچنین می توانید TestScope
خود را برای استفاده با runTest
ایجاد کنید.
هنگام انجام این کار، مطمئن شوید که runTest
در TestScope
که ایجاد کرده اید فراخوانی کنید:
class SimpleExampleTest { val testScope = TestScope() // Creates a StandardTestDispatcher @Test fun someTest() = testScope.runTest { // ... } }
کد بالا یک StandardTestDispatcher
برای TestScope
به طور ضمنی و همچنین یک زمانبندی جدید ایجاد می کند. این اشیاء نیز می توانند به طور صریح ایجاد شوند. اگر نیاز به ادغام آن با تنظیمات تزریق وابستگی دارید، این می تواند مفید باشد.
class ExampleTest { val testScheduler = TestCoroutineScheduler() val testDispatcher = StandardTestDispatcher(testScheduler) val testScope = TestScope(testDispatcher) @Test fun someTest() = testScope.runTest { // ... } }
تزریق یک اسکوپ
اگر کلاسی دارید که کوروتینهایی را ایجاد میکند که باید در طول تستها آنها را کنترل کنید، میتوانید یک Coroutine scope به آن کلاس تزریق کنید و آن را با TestScope
در تستها جایگزین کنید.
در مثال زیر، کلاس UserState
برای ثبت کاربران جدید و دریافت لیست کاربران ثبت شده به یک UserRepository
وابسته است. از آنجایی که این تماسها به UserRepository
در حال تعلیق تماسهای تابع هستند، UserState
از CoroutineScope
تزریق شده برای شروع یک کوروتین جدید در تابع registerUser
خود استفاده میکند.
class UserState( private val userRepository: UserRepository, private val scope: CoroutineScope, ) { private val _users = MutableStateFlow(emptyList<String>()) val users: StateFlow<List<String>> = _users.asStateFlow() fun registerUser(name: String) { scope.launch { userRepository.register(name) _users.update { userRepository.getAllUsers() } } } }
برای آزمایش این کلاس، می توانید هنگام ایجاد شی UserState
، TestScope
از runTest
ارسال کنید:
class UserStateTest { @Test fun addUserTest() = runTest { // this: TestScope val repository = FakeUserRepository() val userState = UserState(repository, scope = this) userState.registerUser("Mona") advanceUntilIdle() // Let the coroutine complete and changes propagate assertEquals(listOf("Mona"), userState.users.value) } }
برای تزریق یک محدوده خارج از تابع تست، به عنوان مثال به یک شی تحت آزمایش که به عنوان یک ویژگی در کلاس تست ایجاد شده است، به ایجاد TestScope خود مراجعه کنید.