نحوه آزمایش واحدها یا ماژول هایی که با جریان ارتباط برقرار می کنند به این بستگی دارد که آیا موضوع مورد آزمایش از جریان به عنوان ورودی یا خروجی استفاده می کند.
- اگر موضوع مورد آزمایش یک جریان را مشاهده کند، میتوانید جریانهایی را در وابستگیهای جعلی ایجاد کنید که میتوانید از طریق آزمایشها آن را کنترل کنید.
- اگر واحد یا ماژول جریانی را در معرض دید قرار دهد، میتوانید یک یا چند مورد منتشر شده از یک جریان را در آزمایش بخوانید و تأیید کنید.
ایجاد یک تولید کننده جعلی
هنگامی که موضوع مورد آزمایش مصرف کننده یک جریان است، یکی از راه های معمول برای آزمایش آن، جایگزینی تولید کننده با یک پیاده سازی جعلی است. به عنوان مثال، با توجه به کلاسی که مخزنی را مشاهده می کند که داده ها را از دو منبع داده در تولید می گیرد:
برای قطعی کردن تست، می توانید مخزن و وابستگی های آن را با یک مخزن جعلی جایگزین کنید که همیشه همان داده های جعلی را منتشر می کند:
برای انتشار یک سری از مقادیر از پیش تعریف شده در یک جریان، از سازنده 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
...
}
اکنون که بر خروجیهای موضوع مورد آزمایش کنترل دارید، میتوانید با بررسی خروجیهای آن بررسی کنید که درست کار میکند.
اثبات انتشار جریان در یک آزمایش
اگر آزمودنی مورد آزمایش جریانی را در معرض دید قرار دهد، آزمون باید در مورد عناصر جریان داده اظهارنظر کند.
بیایید فرض کنیم که مخزن مثال قبلی یک جریان را نشان می دهد:
با آزمایشهای خاص، فقط باید اولین انتشار یا تعداد محدودی از مواردی که از جریان میآیند را بررسی کنید.
شما می توانید با فراخوانی 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)
}
برای جریانهای دادهای که به مجموعه پیچیدهتری از آیتمها نیاز دارند یا تعداد محدودی از آیتمها را بر نمیگردانند، میتوانید از Flow
API برای انتخاب و تبدیل آیتمها استفاده کنید. در اینجا چند نمونه آورده شده است:
// 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()
به صورت داخلی استفاده میکند و تا زمانی که کل لیست نتایج آماده بازگشت شود به حالت تعلیق در میآید.
برای بهم پیوستن اقداماتی که باعث میشود جریان مقادیر منتشر کند و ادعاهایی بر روی مقادیری که منتشر شدهاند، میتوانید به طور مداوم مقادیر یک جریان را در طول آزمایش جمعآوری کنید.
به عنوان مثال، کلاس 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
دریافت می کند. در این مثال، ما آنها را در یک فهرست جمعآوری میکنیم و سپس ادعاهایی را در مورد محتوای آن انجام میدهیم:
@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
را جمعآوری کنید، همانطور که میتوانید هر جریان دیگری را جمعآوری کنید، از جمله با Turbine. تلاش برای جمع آوری و اثبات همه مقادیر میانی در برخی از سناریوهای آزمایشی مطلوب است.
با این حال، ما به طور کلی توصیه میکنیم که StateFlow
بهعنوان یک دارنده داده در نظر بگیرید و به جای آن، ویژگی value
آن را تأیید کنید. به این ترتیب، آزمایشها وضعیت فعلی شی را در یک نقطه زمانی معین تأیید میکنند و به این بستگی ندارند که آیا ادغام اتفاق میافتد یا خیر.
به عنوان مثال، این ViewModel
انتخاب کنید که مقادیر را از یک Repository
جمع آوری می کند و آنها را در یک StateFlow
در معرض UI قرار می دهد:
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
}
کار با StateFlow های ایجاد شده توسط 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.WhileSubscribed
اغلب در مدلهای view استفاده میشوند.
حتی اگر در آزمایش خود بر 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 در اندروید
- Kotlin در اندروید جریان دارد
-
StateFlow
وSharedFlow
- منابع اضافی برای کوروتین ها و جریان کاتلین
نحوه آزمایش واحدها یا ماژول هایی که با جریان ارتباط برقرار می کنند به این بستگی دارد که آیا موضوع مورد آزمایش از جریان به عنوان ورودی یا خروجی استفاده می کند.
- اگر موضوع مورد آزمایش یک جریان را مشاهده کند، میتوانید جریانهایی را در وابستگیهای جعلی ایجاد کنید که میتوانید از طریق آزمایشها آن را کنترل کنید.
- اگر واحد یا ماژول جریانی را در معرض دید قرار دهد، میتوانید یک یا چند مورد منتشر شده از یک جریان را در آزمایش بخوانید و تأیید کنید.
ایجاد یک تولید کننده جعلی
هنگامی که موضوع مورد آزمایش مصرف کننده یک جریان است، یکی از راه های معمول برای آزمایش آن، جایگزینی تولید کننده با یک پیاده سازی جعلی است. به عنوان مثال، با توجه به کلاسی که مخزنی را مشاهده می کند که داده ها را از دو منبع داده در تولید می گیرد:
برای قطعی کردن تست، می توانید مخزن و وابستگی های آن را با یک مخزن جعلی جایگزین کنید که همیشه همان داده های جعلی را منتشر می کند:
برای انتشار یک سری از مقادیر از پیش تعریف شده در یک جریان، از سازنده 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
...
}
اکنون که بر خروجیهای موضوع مورد آزمایش کنترل دارید، میتوانید با بررسی خروجیهای آن بررسی کنید که درست کار میکند.
اثبات انتشار جریان در یک آزمایش
اگر آزمودنی مورد آزمایش جریانی را در معرض دید قرار دهد، آزمون باید در مورد عناصر جریان داده اظهارنظر کند.
بیایید فرض کنیم که مخزن مثال قبلی یک جریان را نشان می دهد:
با آزمایشهای خاص، فقط باید اولین انتشار یا تعداد محدودی از مواردی که از جریان میآیند را بررسی کنید.
شما می توانید با فراخوانی 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)
}
برای جریانهای دادهای که به مجموعه پیچیدهتری از آیتمها نیاز دارند یا تعداد محدودی از آیتمها را بر نمیگردانند، میتوانید از Flow
API برای انتخاب و تبدیل آیتمها استفاده کنید. در اینجا چند نمونه آورده شده است:
// 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()
به صورت داخلی استفاده میکند و تا زمانی که کل لیست نتایج آماده بازگشت شود به حالت تعلیق در میآید.
برای بهم پیوستن اقداماتی که باعث میشود جریان مقادیر منتشر کند و ادعاهایی بر روی مقادیری که منتشر شدهاند، میتوانید به طور مداوم مقادیر یک جریان را در طول آزمایش جمعآوری کنید.
به عنوان مثال، کلاس 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
دریافت می کند. در این مثال، ما آنها را در یک فهرست جمعآوری میکنیم و سپس ادعاهایی را در مورد محتوای آن انجام میدهیم:
@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
را جمعآوری کنید، همانطور که میتوانید هر جریان دیگری را جمعآوری کنید، از جمله با Turbine. تلاش برای جمع آوری و اثبات همه مقادیر میانی در برخی از سناریوهای آزمایشی مطلوب است.
با این حال، ما به طور کلی توصیه میکنیم که StateFlow
بهعنوان یک دارنده داده در نظر بگیرید و به جای آن، ویژگی value
آن را تأیید کنید. به این ترتیب، آزمایشها وضعیت فعلی شی را در یک نقطه زمانی معین تأیید میکنند و به این بستگی ندارند که آیا ادغام اتفاق میافتد یا خیر.
به عنوان مثال، این ViewModel
انتخاب کنید که مقادیر را از یک Repository
جمع آوری می کند و آنها را در یک StateFlow
در معرض UI قرار می دهد:
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
}
کار با StateFlow های ایجاد شده توسط 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.WhileSubscribed
اغلب در مدلهای view استفاده میشوند.
حتی اگر در آزمایش خود بر 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 در اندروید
- Kotlin در اندروید جریان دارد
-
StateFlow
وSharedFlow
- منابع اضافی برای کوروتین ها و جریان کاتلین