Android पर Kotlin फ़्लो की जांच करना

फ़्लो के साथ काम करने वाली यूनिट या मॉड्यूल की जांच करने का तरीका, इस बात पर निर्भर करता है कि टेस्ट किया जा रहा विषय, फ़्लो का इस्तेमाल इनपुट या आउटपुट के तौर पर करता है या नहीं.

  • अगर जांचा जा रहा विषय किसी फ़्लो को देखता है, तो आपके पास नकली डिपेंडेंसी में फ़्लो जनरेट करने का विकल्प होता है. इन फ़्लो को टेस्ट से कंट्रोल किया जा सकता है.
  • अगर यूनिट या मॉड्यूल किसी फ़्लो को एक्सपोज़ करता है, तो टेस्ट में फ़्लो से उत्सर्जित किए गए एक या एक से ज़्यादा आइटम को पढ़ा और पुष्टि की जा सकती है.

नकली प्रोड्यूसर बनाना

जब टेस्ट किया जा रहा विषय किसी फ़्लो का उपभोक्ता होता है, तो उसे टेस्ट करने का एक सामान्य तरीका यह है कि प्रोड्यूसर को नकली तरीके से लागू करने के लिए बदल दिया जाए. उदाहरण के लिए, एक ऐसी क्लास दी गई है जो किसी ऐसे डेटाबेस को मॉनिटर करती है जो प्रोडक्शन में दो डेटा सोर्स से डेटा लेता है:

जांचा जा रहा विषय और डेटा लेयर
पहली इमेज. जांचा जा रहा विषय और डेटा लेयर.

टेस्ट को डिटरमिनिस्टिक बनाने के लिए, रिपॉज़िटरी और उसकी डिपेंडेंसी को किसी ऐसे फ़र्ज़ी रिपॉज़िटरी से बदला जा सकता है जो हमेशा एक ही फ़र्ज़ी डेटा दिखाता है:

डिपेंडेंसी को नकली तरीके से लागू करने से बदल दिया जाता है
दूसरी इमेज. डिपेंडेंसी को नकली तरीके से लागू करने की सुविधा से बदल दिया जाता है.

किसी फ़्लो में पहले से तय की गई वैल्यू की सीरीज़ को एमिट करने के लिए, 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 का इस्तेमाल करना

तीसरे पक्ष की 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 की मदद से 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, 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 पर दावा किया हो, फिर भी आपको एक कलेक्टर बनाना होगा. यह खाली कलेक्टर हो सकता है:

@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)
}

अन्य संसाधन