קוד לבדיקת יחידה שמשתמש בקורוטין מחייב תשומת לב נוספת, כי הביצוע שלהם יכול להיות אסינכרוני ומתרחש בכמה שרשורים. מדריך זה מסביר איך ניתן לבדוק פונקציות השעיה, את מבני הבדיקה שצריך להכיר ואיך ליצור קוד שמשתמש בקורטין לבדיקה.
ממשקי ה-API שבהם נעשה שימוש במדריך זה הם חלק מהספרייה kotlinx.coroutines.test. כדי לקבל גישה לממשקי ה-API האלה, צריך להוסיף את פריט המידע שנוצר בתהליך הפיתוח (Artifact) כתלות לבדיקה לפרויקט.
dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}
הפעלה של פונקציות השעיה בבדיקות
כדי להפעיל פונקציות השעיה בבדיקות, צריך להיות בקורוטין. מאחר שהפונקציות של בדיקת JUnit אינן משעות פונקציות מושעה, עליכם לקרוא לבונה קורוטין בבדיקות שלכם כדי להתחיל הליך של נשיפה (coroutine) חדש.
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
הם הטמעות של CoroutineDispatcher
למטרות בדיקה. אם נוצרות קורוטינים חדשים במהלך הבדיקה, צריך להשתמש ב-TestDispatchers
כדי לאפשר את הביצוע של הקורוטינים החדשים.
יש שתי הטמעות זמינות של TestDispatcher
: StandardTestDispatcher
ו-UnconfinedTestDispatcher
, שמבצעות תזמון שונה של קורוטין שהתחילו לאחרונה. בשתיהן נעשה שימוש ב-TestCoroutineScheduler
כדי לשלוט בזמן הווירטואלי ולנהל קורוטינים שפועלים במסגרת בדיקה.
בבדיקה צריך להיות רק מופע אחד של מתזמן הבקשות, שמשותף לכל TestDispatchers
. למידע נוסף על שיתוף של מתזמנים, ראו החדרת מתזמן הבדיקה.
כדי להפעיל בדיקת קורוטין ברמה העליונה, runTest
יוצר TestScope
, שהוא יישום של CoroutineScope
שתמיד ישתמש ב-TestDispatcher
. אם לא צוין אחרת, TestScope
ייצור StandardTestDispatcher
כברירת מחדל וישתמש בו כדי להריץ את הקורטין לבדיקה ברמה העליונה.
runTest
עוקב אחר הקואוטין שבהמתנה בתור במתזמן המשימות שמשמש את שולח ה-TestScope
שלו, ולא מוחזר כל עוד יש עבודה בהמתנה על המתזמן הזה.
תקן StandardTestDispatcher
כשמתחילים קורוטין חדשים ב-StandardTestDispatcher
, הם נמצאים בתור בכלי לתזמון הבסיסי, כדי לרוץ בכל פעם שאפשר להשתמש ב-thread של הבדיקה. כדי לאפשר לקורוטינים חדשים לרוץ, צריך לתת את שרשור הבדיקה (פינוי מקום כדי לאפשר שימוש בקורוטינים אחרים). ההתנהגות הזו של התכונה 'הבאים בתור' מאפשרת שליטה מדויקת על האופן שבו קורוטין חדשים פועלים במהלך הבדיקה, והיא דומה לתזמון של הקורוטינים בקוד הייצור.
אם אף פעם לא מופקת שרשור הבדיקה במהלך ביצוע הקורוטין לבדיקה ברמה העליונה, כל קורוטין חדשים ירוצו רק אחרי סיום הבדיקה (אבל לפני ש-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
: מקדם את הזמן הווירטואלי בסכום הנתון ומריץ את כל ה-coroutines שתוזמנו לפעול לפני הנקודה הזו בזמן הווירטואלי.runCurrent
: מריץ קורוטינים שתוזמנו בזמן הווירטואלי הנוכחי.
כדי לתקן את הבדיקה הקודמת, אפשר להשתמש ב-advanceUntilIdle
כדי לאפשר לשני הקורוטינים שבהמתנה לבצע את העבודה לפני שהם ממשיכים לטענת נכונות (assertion):
@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
. הפעולה הזו מאפשרת לקורוטין ברמה העליונה להמשיך בטענת הנכוֹנוּת, והבדיקה נכשלת כי יוסי עדיין לא רשום:
@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
) או כדי להתחיל קורוטינים חדשים. כשקוד מופעל על כמה שרשורים במקביל, הבדיקות עלולות להפוך לרעות. יכול להיות קשה לבצע טענות נכונות (assertions) בזמן הנכון או להמתין להשלמת משימות אם הן פועלות בשרשורי רקע שאין לכם שליטה עליהם.
בבדיקות, צריך להחליף את הגורם האחראי הזה במכונות של TestDispatchers
. יש לכך כמה יתרונות:
- הקוד יפעל בשרשור בדיקה יחיד, כך שהבדיקות יהיו דטרמיניסטיות יותר
- יש לך אפשרות לשלוט באופן ההצגה והתזמון של קורוטין חדשים
- שולחי TestDispatchers משתמשים במתזמן זמן וירטואלי, שמדלג על עיכובים באופן אוטומטי ומאפשר לך לקדם את הזמן באופן ידני
שימוש בהחדרת תלות כדי לספק
לסדרנים בכיתות שלך, קל יותר להחליף את הסדרנים האמיתיים
בדיקות. בדוגמאות האלה נכניס CoroutineDispatcher
, אבל אפשר גם
להחדיר את המודל
CoroutineContext
שמאפשרות גמישות רבה יותר במהלך הבדיקות.
בכיתות שמתחילים בהן קורוטין, אפשר גם להזריק CoroutineScope
.
במקום סדרן, כפי שמפורט במאמר החדרת היקף.
.
כברירת מחדל, מערכת TestDispatchers
תיצור מתזמנים חדשים כאשר הם נוצרים. בתוך runTest
, אפשר לגשת לנכס testScheduler
של TestScope
ולהעביר אותו לכל TestDispatchers
חדש שייווצר. כך הם יוכלו לשתף את ההבנה שלהם לגבי הזמן הווירטואלי, ושיטות כמו advanceUntilIdle
יריצו קורוטינים לכל שולחי הבדיקה עד לסיום התהליך.
בדוגמה הבאה, אפשר לראות מחלקה של Repository
שיוצרת קורוטין חדשה באמצעות המוקדן IO
בשיטה initialize
שלה, ומעבירה את המתקשר למוקדן של IO
בשיטה fetchData
שלה:
// 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
כדי לוודא שהקורוטין החדשה התחילה ב-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()) // ... }
runTest
ימתין להשלמת המשך של קורוטין לפני החזרה אם הקורוטינים נמצאים ב-TestDispatcher
שיש לו מתזמן בקשות משותף. בנוסף, הוא ימתין במקרה של קורוטין שהם צאצאים של רמת הקואוטין לבדיקה ברמה העליונה, גם אם הם משתמשים במשלחים אחרים (עד הזמן הקצוב לתפוגה שמוגדר בפרמטר dispatchTimeoutMs
, שהוא 60 שניות כברירת מחדל).
הגדרת השולח הראשי
בבדיקות יחידה מקומית, השרת של Main
שעטוף את ה-thread של ממשק המשתמש של Android לא יהיה זמין, כי הבדיקות האלה מבוצעות ב-JVM מקומי ולא במכשיר Android. אם הקוד בבדיקה מפנה ל-thread הראשי, הוא יגרום לחריגה במהלך בדיקות היחידה.
במקרים מסוימים אפשר להזריק את הסדרן Main
באותו אופן כמו שולחים אחרים, כפי שמתואר בקטע הקודם, וכך אפשר להחליף אותו ב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
ב-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
חדש שייווצר ישתמש באופן אוטומטי במתזמן המשימות של Main
, כולל StandardTestDispatcher
שנוצר על ידי runTest
אם לא יועבר אליו אף שירות אחר.
כך יהיה קל יותר לוודא שיש רק מתזמן אחד בשימוש במהלך הבדיקה. כדי שהשיטה הזו תפעל, צריך ליצור את כל המופעים האחרים של TestDispatcher
אחרי הקריאה ל-Dispatchers.setMain
.
דפוס נפוץ למניעת שכפול של הקוד שמחליף את המוקדן של 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
כברירת מחדל, אבל אפשר להעביר StandardTestDispatcher
כפרמטר אם לא צריך להפעיל כראוי את השירות Main
במחלקת בדיקה נתונה.
אם צריך מופע 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
.
אם לא מחליפים את הסדרן של 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
ישירות כדי להריץ את בדיקת הקורוטין אצל אותו מוקדן.
יצירת היקף בדיקה משלך
כמו במקרה של 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 { // ... } }
החדרת היקף
אם יש לכם כיתה שיוצרת קורוטינים שעליכם לשלוט בהם במהלך
אפשר להזריק היקף של קורוטין למחלקה הזו, ולהחליף אותו
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() } } } }
כדי לבדוק את הכיתה הזו, אפשר לעבור את המבחן בTestScope
מrunTest
במהלך היצירה
האובייקט UserState
:
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 משלכם