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 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 ব্যবহার করা

থার্ড-পার্টি Turbine লাইব্রেরি, একটি সংগ্রহকারী কোরাউটিন তৈরি করার জন্য একটি সুবিধাজনক API অফার করে, সেইসাথে Flow পরীক্ষা করার জন্য অন্যান্য সুবিধাজনক ফিচারও অফার করে:

@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-এর ভ্যালু সংগ্রহ করতে পারবেন, ঠিক যেমন আপনি টারবাইন সহ অন্য যেকোনও ফ্লো সংগ্রহ করতে পারেন। কিছু টেস্টের ক্ষেত্রে সব অন্তর্বর্তী ভ্যালু সংগ্রহ ও অ্যাসার্ট করার চেষ্টা করা ভাল হতে পারে।

তবে, আমরা সাধারণত 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-এর মাধ্যমে তৈরি StateFlow নিয়ে কাজ করা

আগের বিভাগে, 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)
}

অতিরিক্ত রিসোর্স