הדרך שבה בודקים יחידות או מודולים שמתקשרים עם flow תלוי אם הנושא הנבדק משתמש בזרימה כקלט או כפלט.
- אם נושא הבדיקה מבחין בזרימה, אפשר ליצור זרימה בתוך יחסי תלות מזויפים שניתן לשלוט בהם בבדיקות.
- אם היחידה או המודול חושפים זרימה, אפשר לקרוא ולאמת זרימה מספר פריטים שנפלטים מזרימת הבדיקה.
יצירת מפיק מזויף
כשהנושא בבדיקה הוא צרכן של זרימה, אחת הדרכים הנפוצות לבדוק אותו היא להחליף את המפיק בהטמעה מזויפת. לדוגמה, כאשר מזינים של מחלקה שצופה במאגר שלוקח נתונים משני מקורות נתונים סביבת ייצור:
![את הנושא לבדיקה ושכבת הנתונים](https://developer.android.com/static/images/kotlin/flow/sut-data-layer.png?authuser=2&hl=he)
כדי שהבדיקה תהיה דטרמיניסטית, אפשר להחליף את המאגר או יחסי תלות עם מאגר מזויף שתמיד פולט את אותם נתונים מזויפים:
![יחסי התלות מוחלפים בהטמעה מזויפת](https://developer.android.com/static/images/kotlin/flow/fake-implementation.png?authuser=2&hl=he)
כדי לפטור סדרת ערכים מוגדרת מראש ברצף, משתמשים ב-builder flow
:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
במסגרת הבדיקה, המאגר המזויף הזה מוזרק ומחליף את המאגר האמיתי הטמעה:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
עכשיו, כשיש לכם שליטה על הפלט של הנושא שנבדק, אתם יכולים כדי לוודא שהוא פועל כראוי באמצעות בדיקת הפלט שלו.
הצהרת פליטות מתהליכי עבודה בבדיקה
אם הנושא בבדיקה חושף זרימה, הבדיקה צריכה לטעון טענות נכונות (assertions) רכיבים של מקור נתונים.
נניח שמאגר הנתונים בדוגמה הקודמת חושף תהליך:
![מאגר יחסי של יחסי תלות מזויפים שחושף תהליכים](https://developer.android.com/static/images/kotlin/flow/test-exposed-flow.png?authuser=2&hl=he)
בבדיקות מסוימות צריך לבדוק רק את הפליטה הראשונה או לבדוק כמות סופית מספר הפריטים שמגיעים מהתהליך.
אפשר לצרוך את הפליטה הראשונה לזרימה על ידי קריאה אל first()
. הזה
הפונקציה תמתין עד שהפריט הראשון יתקבל ואז תשלח את הביטול
אות למפיק.
@Test
fun myRepositoryTest() = runTest {
// Given a repository that combines values from two data sources:
val repository = MyRepository(fakeSource1, fakeSource2)
// When the repository emits a value
val firstItem = repository.counter.first() // Returns the first item in the flow
// Then check it's the expected item
assertEquals(ITEM_1, firstItem)
}
אם בבדיקה צריך לבדוק כמה ערכים, קריאה לפונקציה toList()
היא שגורמת לזרימה
להמתין עד שהמקור יפלוט את כל הערכים שלו ואז יחזיר את הערכים האלה
חדשה. האפשרות הזו פועלת רק לגבי מקורות נתונים סופיים.
@Test
fun myRepositoryTest() = runTest {
// Given a repository with a fake data source that emits ALL_MESSAGES
val messages = repository.observeChatMessages().toList()
// When all messages are emitted then they should be ALL_MESSAGES
assertEquals(ALL_MESSAGES, messages)
}
לגבי מקורות נתונים שדורשים איסוף מורכב יותר של פריטים או לא מחזירים
מספר מוגבל של פריטים, אפשר להשתמש ב-API של Flow
כדי לבחור ולבצע טרנספורמציה
פריטים. הנה כמה דוגמאות:
// Take the second item
outputFlow.drop(1).first()
// Take the first 5 items
outputFlow.take(5).toList()
// Takes the first item verifying that the flow is closed after that
outputFlow.single()
// Finite data streams
// Verify that the flow emits exactly N elements (optional predicate)
outputFlow.count()
outputFlow.count(predicate)
איסוף רציף במהלך בדיקה
במסגרת איסוף זרם באמצעות toList()
כמו שנראה בדוגמה הקודמת,
collect()
באופן פנימי, ומושעה עד שרשימת התוצאות כולה תהיה מוכנה
הוחזרו.
לשלב פעולות שגורמות לזרימה לפלוט ערכים וטענות נכונות (assertions) שהופקו, אפשר לאסוף ברציפות ערכים מתוך זרימה בדיקה.
לדוגמה, נבחן את המחלקה הבאה Repository
,
הנלווה להטמעה של מקור נתונים מזויף שכולל שיטת emit
כדי
מפיקה ערכים באופן דינמי במהלך הבדיקה:
class Repository(private val dataSource: DataSource) {
fun scores(): Flow<Int> {
return dataSource.counts().map { it * 10 }
}
}
class FakeDataSource : DataSource {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun counts(): Flow<Int> = flow
}
כשמשתמשים בזיוף הזה בבדיקה, אפשר ליצור קוקאין
לקבל באופן רציף את הערכים מה-Repository
. בדוגמה הזאת אנחנו כוללים
לאסוף אותם לרשימה ולאחר מכן להריץ טענות נכונות (assertions) לגבי התוכן שלה:
@Test
fun continuouslyCollect() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
val values = mutableListOf<Int>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
repository.scores().toList(values)
}
dataSource.emit(1)
assertEquals(10, values[0]) // Assert on the list contents
dataSource.emit(2)
dataSource.emit(3)
assertEquals(30, values[2])
assertEquals(3, values.size) // Assert the number of items collected
}
מכיוון שהזרימה שנחשפת על ידי Repository
כאן לא מסתיימת אף פעם, toList
שאוספת אותו אף פעם לא חוזרת. תחילת איסוף הקורוטין בעוד
TestScope.backgroundScope
מבטיחה שהקורוטין תבוטל לפני סיום הבדיקה. אחרת,
האפליקציה runTest
תמשיך להמתין לסיום הבדיקה, וכתוצאה מכך תסתיים הבדיקה
מגיבים ובסופו של דבר להיכשל.
שימו לב איך
UnconfinedTestDispatcher
משמש לאיסוף הקורוטינים כאן. כך אפשר לוודא שהאיסוף
הקואוטין מושקת בפתיחות ומוכנה לקבל ערכים אחרי launch
החזרות.
שימוש בטורבינה
פלטפורמת Turbine של צד שלישי מציעה ממשק API נוח ליצירת סם קורוטין, כתכונות נוחות נוספות לבדיקת תהליכי עבודה:
@Test
fun usingTurbine() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
repository.scores().test {
// Make calls that will trigger value changes only within test{}
dataSource.emit(1)
assertEquals(10, awaitItem())
dataSource.emit(2)
awaitItem() // Ignore items if needed, can also use skip(n)
dataSource.emit(3)
assertEquals(30, awaitItem())
}
}
לצפייה המסמכים של הספרייה עבור פרטים נוספים.
בדיקת StateFlows
StateFlow
הוא ניתן לתצפית
הוא הבעלים של הנתונים, שאפשר לאסוף כדי לבחון את הערכים שהוא מחזיק לאורך זמן
זרם. שימו לב שזרם הערכים הזה מאוחד. כלומר, אם
מוגדרים במהירות StateFlow
, והאוספים של אותו StateFlow
לקבל את כל ערכי הביניים, רק את האחרון.
בבדיקות, אם זוכרים את ההגדרה, אפשר לאסוף את ערכי StateFlow
כמו שאפשר לאסוף כל זרם אחר, כולל באמצעות טורבינה. מתבצע ניסיון לאסוף
ולהצהיר על כל ערכי ביניים בתרחישי בדיקה מסוימים.
עם זאת, באופן כללי מומלץ להתייחס אל StateFlow
כמחזיק בנתונים
ולהצהיר על הנכס value
במקום זאת. באופן הזה, הבדיקות מאמתות את
של האובייקט בנקודת זמן נתונה, ולא תלויים בשאלה אם
באופן כללי.
לדוגמה, ניקח ב-ViewModel, נתונים שאוספים ערכים מ-Repository
וגם
חושפת אותם לממשק המשתמש באמצעות StateFlow
:
class MyViewModel(private val myRepository: MyRepository) : ViewModel() {
private val _score = MutableStateFlow(0)
val score: StateFlow<Int> = _score.asStateFlow()
fun initialize() {
viewModelScope.launch {
myRepository.scores().collect { score ->
_score.value = score
}
}
}
}
יישום מזויף של ה-Repository
הזה עשוי להיראות כך:
class FakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
כשבודקים את ה-ViewModel עם הזיוף הזה, אפשר לפלוט ערכים
להפעיל עדכונים בStateFlow
של ViewModel, ואז להצהיר בעלות
value
:
@Test
fun testHotFakeRepository() = runTest {
val fakeRepository = FakeRepository()
val viewModel = MyViewModel(fakeRepository)
assertEquals(0, viewModel.score.value) // Assert on the initial value
// Start collecting values from the Repository
viewModel.initialize()
// Then we can send in values one by one, which the ViewModel will collect
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value) // Assert on the latest value
}
עבודה עם StateFlows שנוצר על ידי stateIn
בקטע הקודם, ה-ViewModel משתמש ב-MutableStateFlow
כדי לאחסן
הערך האחרון שנפלט על ידי זרימה מה-Repository
. זהו דפוס נפוץ
בדרך כלל בדרך פשוטה יותר באמצעות
stateIn
שממיר זרם קר לStateFlow
חם:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
האופרטור stateIn
כולל פרמטר SharingStarted
שקובע מתי
הוא הופך לפעיל ומתחילה לצרוך את הזרימה הבסיסית. אפשרויות כמו
SharingStarted.Lazily
ו-SharingStarted.WhileSubsribed
בשימוש לעיתים קרובות
ב-ViewModels.
גם אם טענת נכוֹנוּת על value
של StateFlow
בבדיקה, תקבל
צריך ליצור אוסף זה יכול להיות אוסף ריק:
@Test
fun testLazilySharingViewModel() = runTest {
val fakeRepository = HotFakeRepository()
val viewModel = MyViewModelWithStateIn(fakeRepository)
// Create an empty collector for the StateFlow
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.score.collect()
}
assertEquals(0, viewModel.score.value) // Can assert initial value
// Trigger-assert like before
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value)
}
מקורות מידע נוספים
- בדיקת קורוטין של Kotlin ב-Android
- Kotlin ב-Android
StateFlow
ו-SharedFlow
- מקורות מידע נוספים בנושא קורוטין וזרימה ב-Kotlin