फ़्लो से कम्यूनिकेट करने वाली यूनिट या मॉड्यूल को टेस्ट करने का तरीका, इस बात पर निर्भर करता है कि टेस्ट किया जा रहा विषय, फ़्लो को इनपुट या आउटपुट के तौर पर इस्तेमाल करता है या नहीं.
- अगर टेस्ट किए जा रहे विषय में कोई फ़्लो दिखता है, तो फ़ेक डिपेंडेंसी में फ़्लो जनरेट किए जा सकते हैं. इन्हें टेस्ट से कंट्रोल किया जा सकता है.
- अगर यूनिट या मॉड्यूल कोई फ़्लो दिखाता है, तो टेस्ट में फ़्लो से जनरेट हुए एक या एक से ज़्यादा आइटम पढ़े और उनकी पुष्टि की जा सकती है.
फ़र्ज़ी प्रोड्यूसर बनाना
जब टेस्ट किया जा रहा विषय, किसी फ़्लो का उपभोक्ता होता है, तो उसे टेस्ट करने का एक सामान्य तरीका यह है कि प्रोड्यूसर को फ़र्ज़ी तरीके से लागू करने वाले फ़ंक्शन से बदल दिया जाए. उदाहरण के लिए, मान लें कि कोई क्लास, ऐसी रिपॉज़िटरी पर नज़र रखती है जो प्रोडक्शन में दो डेटा सोर्स से डेटा लेती है:
टेस्ट को डिटरमिनिस्टिक बनाने के लिए, रिपॉज़िटरी और उसकी डिपेंडेंसी को ऐसी फ़ेक रिपॉज़िटरी से बदला जा सकता है जो हमेशा एक जैसा फ़ेक डेटा जनरेट करती है:
फ़्लो में पहले से तय की गई वैल्यू की सीरीज़ को दिखाने के लिए, 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 एपीआई का इस्तेमाल करके आइटम चुने और बदले जा सकते हैं. यहां कुछ उदाहरण दिए गए हैं:
// 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 के पूरा होने का इंतज़ार करता रहेगा. इससे टेस्ट काम करना बंद कर देगा और आखिर में फ़ेल हो जाएगा.
ध्यान दें कि यहां collect coroutine के लिए UnconfinedTestDispatcher का इस्तेमाल किया गया है. इससे यह पक्का होता है कि कलेक्ट करने वाली कोरूटीन तुरंत लॉन्च हो जाए और launch के बाद वैल्यू पाने के लिए तैयार हो जाए.
Turbine का इस्तेमाल करना
तीसरे पक्ष की 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 की वैल्यू इकट्ठा की जा सकती हैं. जैसे, किसी अन्य फ़्लो की वैल्यू इकट्ठा की जाती हैं. इसमें टरबाइन का इस्तेमाल भी शामिल है. कुछ टेस्ट के उदाहरणों में, सभी इंटरमीडिएट वैल्यू इकट्ठा करने और उन पर दावा करने की कोशिश करना सही हो सकता है.
हालांकि, हमारा सुझाव है कि 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 की जांच करते समय, फ़र्ज़ी ऑब्जेक्ट से वैल्यू भेजी जा सकती हैं. इससे 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 से बनाए गए StateFlow के साथ काम करना
पिछले सेक्शन में, 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 जैसे विकल्पों का अक्सर इस्तेमाल किया जाता है.
अगर आपको अपने टेस्ट में 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) }
अन्य संसाधन
- Android पर Kotlin कोरूटीन की जांच करना
- Android पर Kotlin फ़्लो
StateFlowऔरSharedFlow- Kotlin coroutines और फ़्लो के बारे में ज़्यादा जानकारी देने वाले संसाधन