যেসব ইউনিট বা মডিউল ফ্লো-এর সাথে যোগাযোগ করে, সেগুলোকে আপনি কীভাবে পরীক্ষা করবেন তা নির্ভর করে পরীক্ষাধীন সত্তাটি ফ্লো-কে ইনপুট হিসেবে ব্যবহার করে নাকি আউটপুট হিসেবে।
- পরীক্ষাধীন সত্তা যদি কোনো ফ্লো পর্যবেক্ষণ করে, তাহলে আপনি নকল ডিপেন্ডেন্সির মধ্যে ফ্লো তৈরি করতে পারেন, যা আপনি টেস্টের মাধ্যমে নিয়ন্ত্রণ করতে পারবেন।
- যদি ইউনিট বা মডিউলটি কোনো ফ্লো প্রকাশ করে, তাহলে আপনি পরীক্ষার সময় সেই ফ্লো দ্বারা নির্গত এক বা একাধিক আইটেম পড়তে ও যাচাই করতে পারবেন।
একটি নকল প্রযোজক তৈরি করা
যখন পরীক্ষাধীন বিষয়টি কোনো ফ্লো-এর কনজিউমার হয়, তখন এটিকে পরীক্ষা করার একটি সাধারণ উপায় হলো প্রডিউসারকে একটি নকল ইমপ্লিমেন্টেশন দিয়ে প্রতিস্থাপন করা। উদাহরণস্বরূপ, এমন একটি ক্লাসের কথা ধরা যাক যা এমন একটি রিপোজিটরিকে পর্যবেক্ষণ করে, যেটি প্রোডাকশনে থাকা দুটি ডেটা সোর্স থেকে ডেটা গ্রহণ করে:

পরীক্ষাটিকে সুনির্দিষ্ট করার জন্য, আপনি রিপোজিটরি এবং এর নির্ভরতাগুলোকে একটি নকল রিপোজিটরি দিয়ে প্রতিস্থাপন করতে পারেন যা সর্বদা একই নকল ডেটা নির্গত করে:

একটি ফ্লো-তে পূর্বনির্ধারিত ধারাবাহিক মান নির্গত করতে, 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 রিটার্ন করার পরেই মান গ্রহণ করার জন্য প্রস্তুত থাকে।
টারবাইন ব্যবহার করে
তৃতীয় পক্ষের টারবাইন লাইব্রেরিটি একটি কালেক্টিং কো-রুটিন তৈরি করার জন্য একটি সুবিধাজনক এপিআই প্রদান করে, সেইসাথে ফ্লো পরীক্ষা করার জন্য অন্যান্য সুবিধাজনক বৈশিষ্ট্যও দেয়:
@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()) } }
আরও বিস্তারিত জানতে লাইব্রেরির ডকুমেন্টেশন দেখুন।
স্টেটফ্লো পরীক্ষা করা
StateFlow হলো একটি পর্যবেক্ষণযোগ্য ডেটা ধারক, যা থেকে সময়ের সাথে সাথে এর ধারণ করা মানগুলোকে একটি প্রবাহ (স্ট্রিম) হিসেবে পর্যবেক্ষণ করা যায়। উল্লেখ্য যে, এই মানের প্রবাহটি মিশ্রিত (কনফ্লেটেড) হয়, যার অর্থ হলো, যদি কোনো StateFlow দ্রুত মান সেট করা হয়, তবে সেই StateFlow এর সংগ্রাহকরা সমস্ত মধ্যবর্তী মান পাবে এমন কোনো নিশ্চয়তা নেই, তারা কেবল সর্বশেষ মানটিই পাবে।
টেস্ট করার সময়, কনফ্লেশনের কথা মাথায় রাখলে, আপনি টারবাইন সহ অন্য যেকোনো ফ্লো-এর মতোই একটি StateFlow -এর ভ্যালু সংগ্রহ করতে পারেন। কিছু কিছু টেস্ট সিনারিওতে, সমস্ত অন্তর্বর্তী ভ্যালু সংগ্রহ করে সেগুলোর অ্যাসার্ট করা বাঞ্ছনীয় হতে পারে।
তবে, আমরা সাধারণত 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 পরীক্ষা করার সময়, আপনি ViewModel এর StateFlow তে আপডেট ট্রিগার করতে ফেকটি থেকে ভ্যালু এমিট করতে পারেন, এবং তারপর আপডেট হওয়া 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 }
stateIn দ্বারা তৈরি StateFlows নিয়ে কাজ করা
পূর্ববর্তী অংশে, ViewModel টি Repository থেকে আসা কোনো ফ্লো দ্বারা নির্গত সর্বশেষ মান সংরক্ষণ করার জন্য একটি MutableStateFlow ব্যবহার করে। এটি একটি প্রচলিত প্যাটার্ন, যা সাধারণত 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 এর মতো অপশনগুলো ভিউ মডেলে প্রায়শই ব্যবহৃত হয়।
আপনার টেস্টে StateFlow এর value যাচাই করার প্রয়োজন হলেও, আপনাকে একটি কালেক্টর তৈরি করতে হবে। এটি একটি খালি কালেক্টরও হতে পারে:
@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) }
অতিরিক্ত সম্পদ
- অ্যান্ড্রয়েডে কোটলিন কোরাউটিন পরীক্ষা করা
- অ্যান্ড্রয়েডে কোটলিন ফ্লো
-
StateFlowএবংSharedFlow - কোটলিন কোরাউটিন এবং ফ্লো সম্পর্কিত অতিরিক্ত রিসোর্স