I've implemented Android view model which exposes Compose State<T> instance. For instance, view model implementation:
class MyViewModel<S> {
private val _viewState: MutableState<S> = mutableStateOf(initialState)
val viewState: State<S> = _viewState
...
protected fun setState(newState: S) {
_viewState.value = newState
}
}
I'd like to test in unit tests what values/state it will get set. Just a brief example of what I'd like to achieve:
class MyViewModelTest {
#Test
fun `when view model initialized then should emit initial state first`() {
val viewModel = MyViewModel()
assertEquals(InitialState(), viewModel.viewState.value)
}
#Test
fun `when view model interacted then should emit result state`() {
val viewModel = MyViewModel()
val expectedState = NewState()
viewModel.setState(expectedState)
assertEquals(expectedState, viewModel.viewState.value)
}
}
Is it possible to test State<T>? How do you guys unit test compose states values if you store them in view model side?
For my specific use case, I'm using coroutines and unidirectional data flow, but exposing state via mutableStateOf. Was looking to Unit Test initial state + effect of events as is your case.
View Model:
class MyViewModel(
// For injecting test dispatcher
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
// State exposed via delegate
var state by mutableStateOf(State.Show())
private set
fun onEvent(event: Event) {
when (event) {
is Event.Greet -> greet(event)
else -> TODO("Other Stuff")
}
}
private fun greet(greet: Event.Greet) {
viewModelScope.launch(dispatcher) {
// Do some, ya know, API stuff!
state = State.Show("Hello ${greet.name}")
}
}
}
state is exposed via delegate, with a private setter. The initial value would be Show("Hello"). We can also set updates directly with the imports:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
The injection of the dispatcher is essential for controlling the progression.
States & Events, for completeness
// Encapsulates possible states
sealed class State {
// Data class calculates equality from the constructor
// aiding with assertEquals
data class Show(
val greeting: String = "Hello"
) : State()
}
// ... and possible Events
sealed class Event {
data class Greet(val name: String) : Event()
}
With this setup, we can get to the unit testing:
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class MyViewModelTest {
val dispatcher = StandardTestDispatcher()
#Test
fun `viewModel shows initial state`() = runTest {
val vm = MyViewModel(dispatcher)
val expectedState = State.Show()
// Get the initial state
val state = vm.state
assertEquals(expectedState, state)
}
#Test
fun `viewModel shows updated state`() = runTest {
val vm = MyViewModel(dispatcher)
val expectedState = State.Show("Hello Robertus")
// Trigger an event
vm.onEvent(Event.Greet("Robertus"))
// Await the change
dispatcher.scheduler.advanceUntilIdle()
// Get the state
val state = vm.state
assertEquals(expectedState, state)
}
}
The val dispatcher = StandardTestDispatcher() lets us pass in a dispatcher we can control for awaiting the coroutine. We pass this to the ViewModel in the tests and when needing to await them to run and update the state.
Note: the tests are within the expression = runTest { }.
For the first test, viewModel shows initial state, we get the initial value the mutableStatOf was initialized with.
In the second test, viewModel shows updated state, we await the coroutine to complete (dispatcher to idle) by calling dispatcher.scheduler.advanceUntilIdle() between passing the Event and getting the state.
Without it, we will get the initial state.
Related
I'm building a KMM app for retrieving news.
My app fetches news every 30 seconds and save it in a local database. User must be logged for use it. When user want to logout i need to stop refreshing news and delete the local database.
How do i stop a flow with an infinite loop properly without use static variabile?
I designed the app like follows:
ViewModel (separate for Android and iOS)
UseCase (shared)
Repository (shared)
Data source (shared)
Android Jetpack compose single activity
iOS SwiftUI
Android ViewModel:(iOS use ObservableObject, but logic is the same)
#HiltViewModel
class NewsViewModel #Inject constructor(
private val startFetchingNews: GetNewsUseCase,
private val stopFetchingNews: StopGettingNewsUseCase,
) : ViewModel() {
private val _mutableNewsUiState = MutableStateFlow(NewsState())
val newsUiState: StateFlow<NewsState> get() = _mutableNewsUiState.asStateFlow()
fun onTriggerEvent(action: MapEvents) {
when (action) {
is NewsEvent.GetNews -> {
getNews()
}
is MapEvents.StopNews -> {
//????
}
else -> {
}
}
}
private fun getNews()() {
startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
}
UseCase:
class GetNewsUseCase(
private val newsRepo: NewsRepoInterface) {
companion object {
private val UPDATE_INTERVAL = 30.seconds
}
operator fun invoke(): CommonFlow<Result<List<News>>> = flow {
while (true) {
emit(Result.loading())
val result = newsRepo.getNews()
if (result.succeeded) {
// emit result
} else {
//emit error
}
delay(UPDATE_INTERVAL)
}
}.asCommonFlow()
}
Repository:
class NewsRepository(
private val sourceNews: SourceNews,
private val cacheNews: CacheNews) : NewsRepoInterface {
override suspend fun getNews(): Result<List<News>> {
val news = sourceNews.fetchNews()
//.....
cacheNews.insert(news) //could be a lot of news
return Result.data(cacheNews.selectAll())
}
}
Flow extension functions:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
) {
onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
}
I tried to move the while loop inside repository, so maybe i can break the loop with a singleton repository, but then i must change the getNews method to flow and collect inside GetNewsUseCase (so a flow inside another flow).
Thanks for helping!
When you call launchIn on a Flow, it returns a Job. Hang on to a reference to this Job in a property, and you can call cancel() on it when you want to stop collecting it.
I don't see the point of the CommonFlow class. You could simply write collectCommon as an extension function of Flow directly.
fun <T> Flow<T>.collectCommon(
coroutineScope: CoroutineScope? = null, // 'viewModelScope' on Android and 'nil' on iOS
callback: (T) -> Unit, // callback on each emission
): Job {
return onEach {
callback(it)
}.launchIn(coroutineScope ?: CoroutineScope(Dispatchers.Main))
}
// ...
private var fetchNewsJob: Job? = null
private fun getNews()() {
fetchNewsJob = startFetchingNews().collectCommon(viewModelScope) { result ->
when {
result.error -> {
//update state
}
result.succeeded -> {
//update state
}
}
}
}
In my opinion, collectCommon should be eliminated entirely because all it does is obfuscate your code a little bit. It saves only one line of code at the expense of clarity. It's kind of an antipattern to create a CoroutineScope whose reference you do not keep so you can manage the coroutines running in it--might as well use GlobalScope instead to be clear you don't intend to manage the scope lifecycle so it becomes clear you must manually cancel the Job, not just in the case of the news source change, but also when the UI it's associated with goes out of scope.
I am developing an android app using Coroutine.
Because I have an experience with the Rx, I can understand Coroutine Flow's concept.
But I don't know about emitting new data to the existing flow.
In Rx, I can use BehaviorSubject and it's onNext() function.
Producer:
class ProfileRepository(
private val profileDao: ProfileDao
) {
init {
val profile = profileDao.getMyProfile()
myProfile.onNext(profile)
}
val myProfile: PublishSubject<Profile> = BehaviorSubject.create()
fun updateMyProfile(profile: Profile) {
myProfile.onNext(profile)
}
}
Subscriber:
class ProfileViewModel(
private val profileRepo: ProfileRepository
) {
val myProfile = MutableLiveData<Profile>()
fun loadMyProfile() {
profileRepo.subscribe {
myProfile.postValue(it)
}
}
}
And other UI (like Dialog) can update the source data like:
class SignInDialog(
private val profileRepo: ProfileRepository
) {
fun onSignInSuccess() {
profileRepo.updateMyProfile(profile)
}
}
I want to do this using Coroutine Flow, Is this possible on Flow?
You are using BehaviorSubject, which emits the most recently item at the time of subscription and then continues the sequence until complete. So basically it is a hot stream of data.
The similar behavior has SharedFlow:
A hotFlow that shares emitted values among all its collectors in a broadcast fashion, so that all collectors get all emitted values. A shared flow is called hot because its active instance exists independently of the presence of collectors.
For your case it will look something like the following:
class ProfileRepository(
private val profileDao: ProfileDao
) {
init {
val profile = profileDao.getMyProfile()
_myProfile.tryEmit(profile)
}
private val _myProfile = MutableSharedFlow<String>(replay = 1, extraBufferCapacity = 64)
val myProfile: SharedFlow<Profile> = _myProfile
fun updateMyProfile(profile: Profile) {
_myProfile.tryEmit(profile)
}
}
class ProfileViewModel(
private val profileRepo: ProfileRepository
) {
val myProfile = MutableLiveData<Profile>()
fun loadMyProfile() = viewModelScope.launch {
profileRepo.myProfile.collect {
myProfile.postValue(it)
}
}
}
viewModelScope - Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared. For ViewModelScope, use
androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 or higher.
I have a view model that takes in an initial ViewState object and has a publicly accessible state variable which can be collected.
class MyViewModel<ViewState>(initialState: ViewState) : ViewModel() {
val state: StateFlow<ViewState> = MutableStateFlow(initialState)
val errorFlow: SharedFlow<String> = MutableSharedFlow()
init {
performNetworkCall()
}
private fun performNetworkCall() = viewModelScope.launch {
Network.makeCall(
"/someEndpoint",
onSuccess = {
(state as MutableStateFlow).tryEmit(<some new state>)
},
onError = {
(errorFlow as MutableSharedFlow).tryEmit("network failure")
}
)
}
}
When observing this state from a fragment, I can see the initial state (loading for example) and I collect the change when the network call completes successfully (for example, to a loaded state.)
However, I am at a loss as to how to observe this emission from my ViewModelUnitTest.
I use kotlin turbine to test emissions for my state and shared flows, but I can only observe emissions that occur after I call viewModel.state.test or viewModel.errorFlow.test.
Since I cannot reference viewModel.state or viewModel.errorFlow prior to initialization of the ViewModel, how can I write a test to validate that my initialization logic performs correctly and emits the expected result based on the mocked behavior of Network.makeCall - whether it be a new emission of state or an errorFlow emission?
Since your network class doesn't have any reference in the view model, it isn't possible to mock/fake Network class' behavior. So, create a reference for Network. To capture what's going on in the init block, you can lazy initialize your view model in the test class. It is described in here as the second way. Roughly, your classes look similar to:
class MyViewModel<ViewState>(
initialState: ViewState,
network: Network
) : ViewModel() {
val state: StateFlow<ViewState> = MutableStateFlow(initialState)
val errorFlow: SharedFlow<String> = MutableSharedFlow()
init {
performNetworkCall()
}
private fun performNetworkCall() = viewModelScope.launch {
network.makeCall(
"/someEndpoint",
onSuccess = {
(state as MutableStateFlow).tryEmit(<some new state>)
},
onError = {
(errorFlow as MutableSharedFlow).tryEmit("network failure")
}
)
}
}
#ExperimentalCoroutinesApi
class MyViewModelTest {
#get:Rule
var coroutinesTestRule = CoroutinesTestRule()
val viewModel by lazy { MyViewModel(/*pass arguments*/) }
}
CoroutineTestRule can be found in here.
My ViewModel has a method which returns a flow of PagingData. In my app, the data is fetched from the remote server, which is then saved to Room (the single source of truth):
fun getChocolates(): Flow<PagingData<Chocolate>> {
val pagingSourceFactory = { dao().getChocolateListData() }
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
maxSize = MAX_MEMORY_SIZE,
enablePlaceholders = false
),
remoteMediator = ChocolateRemoteMediator(
api,
dao
),
pagingSourceFactory = pagingSourceFactory
).flow
}
How do I test this method? I want to test if the returning flow contains the correct data.
What I've tried so far:
#InternalCoroutinesApi
#Test
fun getChocolateListReturnsCorrectData() = runBlockingTest {
val chocolateListDao: ChocolateListDao by inject()
val chocolatesRepository: ChocolatesRepository by inject()
val chocolateListAdapter: ChocolateListAdapter by inject()
// 1
val chocolate1 = Chocolate(
name = "Dove"
)
val chocolate2 = Chocolate(
name = "Hershey's"
)
// 2
// You need to launch here because submitData suspends forever while PagingData is alive
val job = launch {
chocolatesRepository.getChocolateListStream().collectLatest {
chocolateListAdapter.submitData(it)
}
}
// Do some stuff to trigger loads
chocolateListDao.saveChocolate(chocolate1, chocolate2)
// How to read from adapter state, there is also .peek() and .itemCount
assertEquals(listOf(chocolate1, chocolate2).toMutableList(), chocolateListAdapter.snapshot())
// We need to cancel the launched job as coroutines.test framework checks for leaky jobs
job.cancel()
}
I'm wondering if I'm on the right track. Any help would be greatly appreciated!
I found using Turbine from cashapp would be much much easier.(JakeWharton comes to rescue again :P)
testImplementation "app.cash.turbine:turbine:0.2.1"
According to your code I think your test case should looks like:
#ExperimentalTime
#ExperimentalCoroutinesApi
#Test
fun `test if receive paged chocolate data`() = runBlockingTest {
val expected = listOf(
Chocolate(name = "Dove"),
Chocolate(name = "Hershey's")
)
coEvery {
dao().getChocolateListData()
}.returns(
listOf(
Chocolate(name = "Dove"),
Chocolate(name = "Hershey's")
)
)
launchTest {
viewModel.getChocolates().test(
timeout = Duration.ZERO,
validate = {
val collectedData = expectItem().collectData()
assertEquals(expected, collectedData)
expectComplete()
})
}
}
I also prepare a base ViewModelTest class for taking care of much of setup and tearDown tasks:
abstract class BaseViewModelTest {
#get:Rule
open val instantTaskExecutorRule = InstantTaskExecutorRule()
#get:Rule
open val testCoroutineRule = CoroutineTestRule()
#MockK
protected lateinit var owner: LifecycleOwner
private lateinit var lifecycle: LifecycleRegistry
#Before
open fun setup() {
MockKAnnotations.init(this)
lifecycle = LifecycleRegistry(owner)
every { owner.lifecycle } returns lifecycle
}
#After
fun tearDown() {
clearAllMocks()
}
protected fun initCoroutine(vm: BaseViewModel) {
vm.apply {
setViewModelScope(testCoroutineRule.testCoroutineScope)
setCoroutineContext(testCoroutineRule.testCoroutineDispatcher)
}
}
#ExperimentalCoroutinesApi
protected fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineRule.runBlockingTest(block)
protected fun launchTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineRule.testCoroutineScope.launch(testCoroutineRule.testCoroutineDispatcher) { block }
}
As for extension function collectData() that's borrowed from answer from another post (Thanks #Farid!!)
And a slide show introducing turbine
There's basically two approaches to this depending on if you want pre-transformation or post-transformation data.
If you want to just assert the repository end, that your query is correct - you can just query PagingSource directly, this is pre-transform though so any mapping you do or filtering you do to PagingData in ViewModel won't be accounted for here. However, it's more "pure" if you want to test the query directly.
#Test
fun repo() = runBlockingTest {
val pagingSource = MyPagingSource()
val loadResult = pagingSource.load(...)
assertEquals(
expected = LoadResult.Page(...),
actual = loadResult,
)
}
The other way if you care about transforms, you need to load data from PagingData into a presenter API.
#Test
fun ui() = runBlockingTest {
val viewModel = ... // Some AndroidX Test rules can help you here, but also some people choose to do it manually.
val adapter = MyAdapter(..)
// You need to launch here because submitData suspends forever while PagingData is alive
val job = launch {
viewModel.flow.collectLatest {
adapter.submitData(it)
}
}
... // Do some stuff to trigger loads
advanceUntilIdle() // Let test dispatcher resolve everything
// How to read from adapter state, there is also .peek() and .itemCount
assertEquals(..., adapter.snapshot())
// We need to cancel the launched job as coroutines.test framework checks for leaky jobs
job.cancel()
}
I am using MVVM in my app. When you enter a query and click search button, the chain is as follows: Fragment -> ViewModel -> Repository -> API -> Client. The client is where HTTP requests are made. But there is one thing here, the client needs to make a call and get a key from the server at initialization. Therefore, to prevent any call before it this first call completes, I need to be able to observe it from Fragment so that I can disable search button. Since each component in the chain can communicate with adjacent components, all components should have a state.
I am thinking to implement a StatefulComponent class and make all components to extend it:
open class StatefulComponent protected constructor() {
enum class State {
CREATED, LOADING, LOADED, FAILED
}
private val currentState = MutableLiveData(State.CREATED)
fun setState(newState: State) {
currentState.value = newState
}
val state: LiveData<State> = currentState
val isLoaded: Boolean = currentState.value == State.LOADED
val isFailed: Boolean = currentState.value == State.FAILED
val isCompleted: Boolean = isLoaded || isFailed
}
The idea is that each component observers the next one and updates itself accordingly. However, this is not possible for ViewModel since it is already extending ViewModel super class.
How can I implement a solution for this problem?
The most common approach is to use sealed class as your state, so you have any paramaters as you want on each state case.
sealed class MyState {
object Loading : MyState()
data class Loaded(data: Data) : MyState()
data class Failed(message: String) : MyState()
}
On your viewmodel you will have only 1 livedata
class MyViewModel : ViewModel() {
private val _state = MutableLiveData<MyState>()
val state: LiveData<MyState> = _state
fun load() {
_state.postCall(Loading)
repo.loadSomeData(onData = { data ->
_state.postCall(Loaded(data))
}, onError = { error -> _state.postCall(Failed(error.message)) })
}
// coroutines approach
suspend fun loadSuspend() {
_state.postCall(Loading)
try {
_state.postCall(Loaded(repo.loadSomeDataSupend()))
} catch(e: Exception) {
_state.postCall(Failed(e.message))
}
}
}
And on the fragment, just observe the state
class MyFragment : Fragment() {
...
onViewCreated() {
viewModel.state.observer(Observer {
when (state) {
// auto casts to each state
Loading -> { button.isEnabled = false }
is Loaded -> { ... }
is Failed -> { ... }
}
}
)
}
}
As João Gouveia mentioned, we can make stateful components quite easily using kotlin's sealed classes.
But to make it further more useful, we can introduce Generics! So, our state class becomes StatefulData<T> which you can use pretty much anywhere (LiveData, Flows, or even in Callbacks).
sealed class StatefulData<out T : Any> {
data class Success<T : Any>(val result : T) : StatefulData<T>()
data class Error(val msg : String) : StatefulData<Nothing>()
object Loading : StatefulData<Nothing>()
}
I've wrote an article fully explaining this particular implementation here
https://naingaungluu.medium.com/stateful-data-on-android-with-sealed-classes-and-kotlin-flow-33e2537ccf55
If you are using the composable ... You can use produce state
#Composable
fun PokemonDetailScreen(
viewModel: PokemonDetailVm = hiltViewModel()
) {
/**
* This takes a initial state and with that we get a coroutine scope where we can call a API and assign the data into the value
*/
val pokemonInfo = produceState<Resource<Pokemon>>(initialValue = Resource.Loading()) {
value = viewModel.getPokemonInfo(pokemonName)
}.value
}