בדיקת קורוטין של Kotlin ב-Android

קוד לבדיקת יחידה שמשתמש בקורוטין מחייב תשומת לב נוספת, כי הביצוע שלהם יכול להיות אסינכרוני ומתרחש בכמה שרשורים. מדריך זה מסביר איך ניתן לבדוק פונקציות השעיה, את מבני הבדיקה שצריך להכיר ואיך ליצור קוד שמשתמש בקורטין לבדיקה.

ממשקי ה-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 משלכם

מקורות מידע נוספים