Setup:
In our project (at work - I cannot post real code), we have implemented clean MVVM. Views communicate with ViewModels via LiveData. ViewModel hosts two kinds of use cases: 'action use cases' to do something, and 'state updater use cases'. Backward communication is asynchronous (in terms of action reaction). It's not like an API call where you get the result from the call. It's BLE, so after writing the characteristic there will be a notification characteristic we listen to. So we use a lot of Rx to update the state. It's in Kotlin.
ViewModel:
#PerFragment
class SomeViewModel #Inject constructor(private val someActionUseCase: SomeActionUseCase,
someUpdateStateUseCase: SomeUpdateStateUseCase) : ViewModel() {
private val someState = MutableLiveData<SomeState>()
private val stateSubscription: Disposable
// region Lifecycle
init {
stateSubscription = someUpdateStateUseCase.state()
.subscribeIoObserveMain() // extension function
.subscribe { newState ->
someState.value = newState
})
}
override fun onCleared() {
stateSubscription.dispose()
super.onCleared()
}
// endregion
// region Public Functions
fun someState() = someState
fun someAction(someValue: Boolean) {
val someNewValue = if (someValue) "This" else "That"
someActionUseCase.someAction(someNewValue)
}
// endregion
}
Update state use case:
#Singleton
class UpdateSomeStateUseCase #Inject constructor(
private var state: SomeState = initialState) {
private val statePublisher: PublishProcessor<SomeState> =
PublishProcessor.create()
fun update(state: SomeState) {
this.state = state
statePublisher.onNext(state)
}
fun state(): Observable<SomeState> = statePublisher.toObservable()
.startWith(state)
}
We are using Spek for unit tests.
#RunWith(JUnitPlatform::class)
class SomeViewModelTest : SubjectSpek<SomeViewModel>({
setRxSchedulersTrampolineOnMain()
var mockSomeActionUseCase = mock<SomeActionUseCase>()
var mockSomeUpdateStateUseCase = mock<SomeUpdateStateUseCase>()
var liveState = MutableLiveData<SomeState>()
val initialState = SomeState(initialValue)
val newState = SomeState(newValue)
val behaviorSubject = BehaviorSubject.createDefault(initialState)
subject {
mockSomeActionUseCase = mock()
mockSomeUpdateStateUseCase = mock()
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(behaviorSubject)
SomeViewModel(mockSomeActionUseCase, mockSomeUpdateStateUseCase).apply {
liveState = state() as MutableLiveData<SomeState>
}
}
beforeGroup { setTestRxAndLiveData() }
afterGroup { resetTestRxAndLiveData() }
context("some screen") {
given("the action to open the screen") {
on("screen opened") {
subject
behaviorSubject.startWith(initialState)
it("displays the initial state") {
assertEquals(liveState.value, initialState)
}
}
}
given("some setup") {
on("some action") {
it("does something") {
subject.doSomething(someValue)
verify(mockSomeUpdateStateUseCase).someAction(someOtherValue)
}
}
on("action updating the state") {
it("displays new state") {
behaviorSubject.onNext(newState)
assertEquals(liveState.value, newState)
}
}
}
}
}
At first we were using an Observable instead of the BehaviorSubject:
var observable = Observable.just(initialState)
...
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(observable)
...
observable = Observable.just(newState)
assertEquals(liveState.value, newState)
instead of the:
val behaviorSubject = BehaviorSubject.createDefault(initialState)
...
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(behaviorSubject)
...
behaviorSubject.onNext(newState)
assertEquals(liveState.value, newState)
but the unit test were being flaky. Mostly they would pass (always when ran in isolation), but sometime they would fail when running the whole suit. Thinking it is to do with asynchronous nature of the Rx we moved to BehaviourSubject to be able to control when the onNext() happens. Test are now passing when we run them from AndroidStudio on the local machine, but they are still flaky on the build machine. Restarting the build often makes them pass.
The tests which fail are always the ones where we assert the value of LiveData. So the suspects are LiveData, Rx, Spek or their combination.
Question: Did anyone have similar experiences writing unit tests with LiveData, using Spek or maybe Rx, and did you find ways to write them which solve these flakiness issues?
....................
Helper and extension functions used:
fun instantTaskExecutorRuleStart() =
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) {
runnable.run()
}
override fun isMainThread(): Boolean {
return true
}
override fun postToMainThread(runnable: Runnable) {
runnable.run()
}
})
fun instantTaskExecutorRuleFinish() = ArchTaskExecutor.getInstance().setDelegate(null)
fun setRxSchedulersTrampolineOnMain() = RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
fun setTestRxAndLiveData() {
setRxSchedulersTrampolineOnMain()
instantTaskExecutorRuleStart()
}
fun resetTestRxAndLiveData() {
RxAndroidPlugins.reset()
instantTaskExecutorRuleFinish()
}
fun <T> Observable<T>.subscribeIoObserveMain(): Observable<T> =
subscribeOnIoThread().observeOnMainThread()
fun <T> Observable<T>.subscribeOnIoThread(): Observable<T> = subscribeOn(Schedulers.io())
fun <T> Observable<T>.observeOnMainThread(): Observable<T> =
observeOn(AndroidSchedulers.mainThread())
I didn't used Speck for unit-testing. I've used java unit-test platform and it works perfect with Rx & LiveData, but you have to keep in mind one thing. Rx & LiveData are async and you can't do something like someObserver.subscribe{}, someObserver.doSmth{}, assert{} this will work sometimes but it's not the correct way to do it.
For Rx there's TestObservers for observing Rx events. Something like:
#Test
public void testMethod() {
TestObserver<SomeObject> observer = new TestObserver()
someClass.doSomethingThatReturnsObserver().subscribe(observer)
observer.assertError(...)
// or
observer.awaitTerminalEvent(1, TimeUnit.SECONDS)
observer.assertValue(somethingReturnedForOnNext)
}
For LiveData also, you'll have to use CountDownLatch to wait for LiveData execution. Something like this:
#Test
public void someLiveDataTest() {
CountDownLatch latch = new CountDownLatch(1); // if you want to check one time exec
somethingTahtReturnsLiveData.observeForever(params -> {
/// you can take the params value here
latch.countDown();
}
//trigger live data here
....
latch.await(1, TimeUnit.SECONDS)
assert(...)
}
Using this approach your test should run ok in any order on any machine. Also the wait time for latch & terminal event should be as low as possible, the tests should run fast.
Note1: The code is in JAVA but you can change it easily in kotlin.
Note2: Singleton are the biggest enemy of unit-testing ;). (With static methods by their side).
The issue is not with LiveData; it is the more common problem - singletons. Here the Update...StateUseCases had to be singletons; otherwise if observers got a different instance they would have a different PublishProcessor and would not get what was published.
There is a test for each Update...StateUseCases and there is a test for each ViewModel into which Update...StateUseCases is injected (well indirectly via the ...StateObserver).
The state exists within the Update...StateUseCases, and since it is a singleton, it gets changed in both tests and they use the same instance becoming dependent on each other.
Firstly try to avoid using singletons if possible.
If not, reset the state after each test group.
Related
I have a ViewModel that uses StateFlow.asLiveData() to expose a Repository class's StateFlow items and I'm trying to write a test for the ViewModel. My tests are configured with a Mock of an Observer<LoadingStatus> on the ViewModel's exposed LiveData.
The code I'm testing calls this method to update its loading status:
suspend fun MutableStateFlow<LoadingStatus>.performWithStatusUpdates(operation: suspend () -> Unit) {
this.value = LoadingStatus.Loading()
try {
operation.invoke()
this.value = LoadingStatus.Success()
} catch (e: Throwable) {
this.value = LoadingStatus.Error(e)
}
}
My tests look something like this:
fun testSomething() = runTest {
viewModel.doSomething()
advanceUntilIdle()
argumentCaptor<LoadingStatus>().apply {
verify(loadingStatusObserver, atLeast(2)).onChanged(capture())
assertTrue(allValues.any { it is LoadingStatus.Loading })
assertTrue(allValues.any { it is LoadingStatus.Success })
}
}
The ViewModel contains code like this:
val loadingStatus = repository.loadingStatusObservable.asLiveData(
viewModelScope.coroutineContext + ioDispatcher
) // when running tests, ioDispatcher is a StandardTestDispatcher passed into the viewModel
fun doSomething() {
viewModelScope.launch(ioDispatcher) {
repository.doSomething()
}
}
And the repository does something like this:
val loadingStatusObservable = MutableStateFlow<LoadingStatus>(LoadingStatus.Idle())
suspend fun doSomething() {
loadingStatusObservable.performWithStatusUpdates {
apiService.doSomethingElse()
}
}
The repository has similar tests that call doSomething() and verify that the status goes to loading and then success, and they pass, but the view model ones fail to pick up the Loading status. If I comment out the line in performWithStatusUpdates that sets the status to success after the operation, the tests do pick up the Loading status, so I'm convinced it's something to do with timing. I have run this code with print statements and debuggers and verified that the status is updating correctly, but the change isn't getting picked up by the observers.
How can I make an observer on a StateFlow.asLiveData() detect all changes, even when they're quickly followed by another, different change?
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'm trying to test my server call with retrofit and rxJava. I'm using a MVP pattern with koin, and I'm having some problems when I try to test the method that do the call to get the data from the server.
I have a prenter that call the interactor to retrieve the data. Interactor DI is did with koin.
I've done some research here and in google and all the examples that I've been watching don't work for me.
The error that I have is this:
Wanted but not invoked:
callback.onResponseSearchFilm(
[Film(uid=1, id=1724, title=The incredible Hulk, tagline=You'll like him when he's angry., overview=Scientist Bruce Banner scours the planet for an antidote to the unbridled force of rage within..., popularity=22.619048, rating=6.1, ratingCount=4283, runtime=114, releaseDate=2008-06-12, revenue=163712074, budget=150000000, posterPath=/bleR2qj9UluYl7x0Js7VXuLhV3s.jpg, originalLanguage=en, genres=null, cast=null, poster=null, favourite=false), Film(uid=2, id=1724, title=The incredible Hulk, tagline=You'll like him when he's angry., overview=Scientist Bruce Banner scours the planet for an antidote to the unbridled force of rage within..., popularity=22.619048, rating=8.0, ratingCount=4283, runtime=114, releaseDate=2008-06-12, revenue=163712074, budget=150000000, posterPath=/bleR2qj9UluYl7x0Js7VXuLhV3s.jpg, originalLanguage=en, genres=null, cast=null, poster=null, favourite=false), Film(uid=3, id=1724, title=The incredible Hulk, tagline=You'll like him when he's angry., overview=Scientist Bruce Banner scours the planet for an antidote to the unbridled force of rage within..., popularity=22.619048, rating=8.5, ratingCount=4283, runtime=114, releaseDate=2008-06-12, revenue=163712074, budget=150000000, posterPath=/bleR2qj9UluYl7x0Js7VXuLhV3s.jpg, originalLanguage=en, genres=null, cast=null, poster=null, favourite=false)]
);
-> at com.filmfy.SearchImplTest.loadItems_WhenDataIsAvailable(SearchImplTest.kt:30)
Actually, there were zero interactions with this mock.
This is my test
class SearchImplTest: KoinTest {
private val searchImpl: SearchImpl = mock()
private val callback: SearchContract.Callback? = mock()
private val api: RetrofitAdapter = mock()
#Test
fun loadItems_WhenDataIsAvailable() {
`when`(api.getFilms()).thenReturn(Observable.just(filmRequestFacke()))
searchImpl.getfilms(callback)
verify(callback)?.onResponseSearchFilm(fackeFilms())
}
}
My interactor code:
class SearchImpl : AbstractInteractor() {
private val voucherApiServe by lazy {
RetrofitAdapter.create()
}
fun getfilms(callback: SearchContract.Callback?){
disposable = voucherApiServe.getFilms()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result -> processFilmSearch(result.data, callback)},
{ error -> processError(error) }
)
}
fun processFilmSearch(filmList : ArrayList<Film>?, callback: SearchContract.Callback?){
callback?.onResponseSearchFilm(filmList)
}
.
.
.
My module with koin:
factory<SearchContract.Presenter> { (view: SearchContract.View) -> SearchPresenter(view, mSearchImpl = get()) }
Api call
#GET(Api.ENDPOINT.FILMS)
fun getFilms(): Observable<FilmRequest>
It is because during unit tests system call your method
searchImpl.getfilms(callback)
and before it will finish immediately call
verify(callback)?.onResponseSearchFilm(fackeFilms())
so getfilms() method not invoked and your test fail.
To wait until your rx code will finish you should inject and replace your Schedulers during unit test.
Change code:
fun getfilms(callback: SearchContract.Callback?){
disposable = voucherApiServe.getFilms()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result -> processFilmSearch(result.data, callback)},
{ error -> processError(error) }
)
}
to:
fun getfilms(callback: SearchContract.Callback?){
disposable = voucherApiServe.getFilms()
.subscribeOn(ioScheduler) //injected scheduler
.observeOn(mainScheduler) //injected scheduler
.subscribe(
{ result -> processFilmSearch(result.data, callback)},
{ error -> processError(error) }
)
}
create Dagger module like:
#Module
class SchedulersModule {
#Provides
#Named(Names.MAIN)
fun main(): Scheduler {
return AndroidSchedulers.mainThread()
}
#Provides
#Named(Names.IO)
fun io(): Scheduler {
return Schedulers.io()
}
#Provides
#Named(Names.COMPUTATION)
fun computation(): Scheduler {
return Schedulers.computation()
}
}
where Names is just a file with string constants (which ofcource as we know have to be different)
and in your SearchImpl class inject this schedulers in constructor.
When you will create your SearchImpl class under test use TestScheduler to replace schedulers inside your voucherApiServe.getFilms() chain.
So. The last part is to force rxjava's schedulers to finish work before you will verify result.
your test should look like this:
import io.reactivex.schedulers.TestScheduler
val testScheduler = TestScheduler()
#Before
fun before() {
//you create your SearchImpl class here and use testScheduler to replace real schedulers inside it
}
#Test
fun loadItems_WhenDataIsAvailable() {
`when`(api.getFilms()).thenReturn(Observable.just(filmRequestFacke()))
searchImpl.getfilms(callback)
testScheduler.triggerActions() //Triggers any actions that have not yet been triggered and that are scheduled to be triggered at or before this Scheduler's present time.
verify(callback)?.onResponseSearchFilm(fackeFilms())
}
So this test will work. This also will help you during UI tests (to remove all delays in Observable.timer for example).
Hope it'll help :)
I have a retrofit service
interface Service {
#PUT("path")
suspend fun dostuff(#Body body: String)
}
It is used in android view model.
class VM : ViewModel(private val service: Service){
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
val state = MutableLiveData<String()
init {
uiScope.launch {
service.doStuff()
state.value = "lol"
}
}
override fun onCleared(){
viewModelJob.cancel()
}
}
I would like to write a test for the cancelling of the view model. This will be done mocking service and delaying so that the co routine does not complete. Whilst blocking, we invoke onCleared to cancel the co routine. This should prevent state getting set...
#Test
fun `on cleared - cancels request`() = runBlocking {
//given
`when`(service.doStuff()).thenAnswer { launch { delay(1000) } }
val vm = ViewModel(service)
// when
vm.cleared()
//then
assertThat(vm.state, nullValue())
}
However it seems that vm.state always gets set??? What is the best way to test when clearing a scope that a co routine gets cancelled?
The problem here is in thenAnswer { launch { delay(1000) } }, which effectively makes your doStuff method look like that:
suspend fun doStuff() {
launch { delay(1000) }
}
As you can see, this function does not actually suspend, it launches a coroutine and returns immediately. What would actually work here is thenAnswer { delay(1000) }, which does not work, because there is no suspend version of thenAnswer in Mockito (as far as I know at least).
I would recommend to switch to Mokk mocking library, which supports kotlin natively. Then you can write coEvery { doStuff() } coAnswers { delay(1000) } and it will make your test pass (after fixing all the syntax errors ofc).
I'm new to writing tests and using Mockito.
I've read the similar topics here on Stackoverflow and made the suggested changes, making sure that regarded classes / interfaces / methods are open.
I tried to follow this
Mocking the constructor injected dependencies
This is the test I came up with so far
class RegistrationPresenterTest {
#Test
fun testRegisterSuccess() {
val mockService = mock<IHerokuInteractor>()
val mockLocal = mock<ILocalStorageInteractor>()
val mockView = mock<RegisterView>()
val mockRegistrationResponse = HerokuRegisterResponse("hash")
val mockPair = ImeiPair("imei","hash")
val presenter = RegisterPresenterImpl(mockLocal,mockService)
whenever(mockService.register(any())).thenReturn(Observable.just(mockRegistrationResponse))
whenever(mockLocal.clearPreferences()).thenReturn(Observable.just(true))
whenever(mockLocal.putImeiPair(any())).thenReturn(Observable.just(true))
//whenever(presenter.writeImeiPairLocally(any())) How do I specify parameters since it uses a parameter from the register method?
presenter.bindView(mockView)
presenter.register("imei","male")
verify(mockService, times(1)).register(any())
verify(mockLocal,times(1)).clearPreferences()
verify(mockLocal,times(1)).putImeiPair(any())
verify(mockView,times(1)).moveToMain()
}
but the response I keep getting is
Wanted but not invoked:
registerPresenterImpl.writeImeiPairLocally(
<any com.company.appname.model.ImeiPair>
);
Actually, there were zero interactions with this mock.
I got this response even when I don't mention that method in the test.
This is my presenter register method. I've changed the classes / interfaces & methods involved to open (kotlin). I believe override methods are open by nature in kotlin.
open class RegisterPresenterImpl #Inject constructor(val localStorage : ILocalStorageInteractor, var herokuService : IHerokuInteractor)
override fun register(imei : String, gender : String){
subscription = herokuService.register(RegisterObject(imei,gender)).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(
{
registrationResult ->
Log.d(TAG,"${registrationResult}")
if(registrationResult.imei_hash != null){
writeImeiPairLocally(ImeiPair(imei,registrationResult.imei_hash))
}
else{
Log.e(TAG,"User already exists")
}
},
{
errorResponse -> Log.e(TAG,"Could not register user ${errorResponse.message}")
}
)
addSubscription(subscription)
}
and similarly the
open fun writeImeiPairLocally(pair : ImeiPair){
subscription = localStorage.clearPreferences().flatMap {
cleared -> localStorage.putImeiPair(pair)}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(
{
booleanResult -> view?.moveToMain()
},
{
errorResponse -> Log.e(TAG,"Could not write ImeiPair to SharedPreferences ${errorResponse.message}")
}
)
addSubscription(subscription)
}
Here is interfaces
open interface ILocalStorageInteractor : ILocalStorage{
fun getImeiPair() : Observable<ImeiPair>
fun putImeiPair(pair: ImeiPair) : Observable<Boolean>
}
open interface ILocalStorage {
fun clearPreferences() : Observable<Boolean>
}
All help is appreciated.
If you are using plain jUnit, then your AndroidSchedulers.mainThread() is null. That's why onNext is not called.
You need to override Schedulers in a setUp() method with:
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
#Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate(); // or .test()
}
});
To avoid concurrency in tests, I would recommend to override Schedulers.io() like this:
RxJavaHooks.setOnIOScheduler(scheduler1 -> Schedulers.immediate());
If you are going to use TestScheduler, don't forget to call TestScheduler.triggerActions() method.
Also don't forget to unregister Schedulers in tearDown() like this:
RxJavaHooks.reset();
RxAndroidPlugins.getInstance().reset();
AndroidSchedulers.reset();
Schedulers.reset();