نحوه آزمایش واحدها یا ماژولهایی که با جریان ارتباط برقرار میکنند به این بستگی دارد که آیا موضوع تحت آزمایش از جریان بهعنوان ورودی یا خروجی استفاده میکند یا نه.
- اگر موضوع تحت آزمایش جریانی را مشاهده کند، میتوانید جریانهایی را در وابستگیهای جعلی که میتوانید از آزمایشها کنترل کنید تولید کنید.
- اگر واحد یا مدولی جریانی را آشکار کند، میتوانید یک یا چند مورد منتشرشده توسط جریان را در آزمایش بخوانید و درستیسنجی کنید.
ایجاد یک تولیدکننده جعلی
وقتی موضوع تحت آزمایش مصرفکننده یک جریان است، یکی از روشهای رایج برای آزمایش آن جایگزین کردن تولیدکننده با یک پیادهسازی جعلی است. برای مثال، با درنظر گرفتن کلاسی که مخزنی را مشاهده میکند که دادهها را از دو منبع داده در تولید میگیرد:
برای قطعی کردن آزمایش، میتوانید مخزن و وابستگیهای آن را با مخزن ساختگی که همیشه دادههای ساختگی یکسانی منتشر میکند جایگزین کنید:
برای انتشار مجموعهای از مقادیر ازپیشتعریفشده در یک جریان، از سازنده 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 myRepositoryTestList() = runTest { val repository = MyFakeRepository() // 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 دریافت میکند. در این مثال، آنها را در فهرستی جمعآوری میکنیم و سپس ادعاهایی درباره محتوای آن انجام میدهیم:
@OptIn(ExperimentalCoroutinesApi::class) @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 میانای برنامهسازی کاربردی مناسبی برای ایجاد روتین همکار جمعآوری ارائه میدهد، همچنین ویژگیهای مناسب دیگری برای آزمایش «جریانها» ارائه میدهد:
@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 در اختیار رابط کاربری قرار میدهد:
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.WhileSubscribed اغلب در
مدلهای نمایشی استفاده میشوند.
حتی اگر در value از StateFlow در آزمایشتان ادعای مالکیت میکنید، باید
گردآورنده ایجاد کنید. این میتواند یک جمعکننده خالی باشد:
@OptIn(ExperimentalCoroutinesApi::class) @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- آزمایش روتینهای همکار