From 0c4c4df8087424c9d587dc4ce426dc8cd67c167c Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Wed, 26 Jan 2022 09:46:20 +0530 Subject: [PATCH] Implemented GithubReposVMTest #2 --- .../ui/github/repolist/GithubReposVMTest.kt | 54 ++++++++-------- .../feat/githubrepos/utils/LiveDataUtil.kt | 63 +++++++++++++++++++ 2 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 ui-githubrepos/src/test/java/com/mutualmobile/feat/githubrepos/utils/LiveDataUtil.kt diff --git a/ui-githubrepos/src/test/java/com/mutualmobile/feat/githubrepos/ui/github/repolist/GithubReposVMTest.kt b/ui-githubrepos/src/test/java/com/mutualmobile/feat/githubrepos/ui/github/repolist/GithubReposVMTest.kt index 8ea118f6..9b758600 100644 --- a/ui-githubrepos/src/test/java/com/mutualmobile/feat/githubrepos/ui/github/repolist/GithubReposVMTest.kt +++ b/ui-githubrepos/src/test/java/com/mutualmobile/feat/githubrepos/ui/github/repolist/GithubReposVMTest.kt @@ -4,16 +4,17 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.paging.PagingData import app.cash.turbine.test import com.mutualmobile.feat.githubrepos.ui.model.UIRepoMapper +import com.mutualmobile.feat.githubrepos.utils.getOrAwaitValue import com.mutualmobile.praxis.data.repository.GithubRepoImpl import com.mutualmobile.praxis.domain.model.DOMOwner import com.mutualmobile.praxis.domain.model.DOMRepo import com.mutualmobile.praxis.domain.usecases.GetGithubTrendingReposUseCase -import com.mutualmobile.praxis.navigator.asFlow import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -29,7 +30,10 @@ import org.junit.rules.TestRule @ExperimentalCoroutinesApi class GithubReposVMTest { - // Important for your test's execution while testing LiveData + /** + * Important for your test's execution while testing LiveData. + * Fixes error message: "Android looper not mocked" + */ @get:Rule var rule: TestRule = InstantTaskExecutorRule() @@ -58,35 +62,33 @@ class GithubReposVMTest { @Test fun `test that trendingGithubRepos() completes without exception`() = runTest { launch { - val testRepo = flowOf( - PagingData.from( - listOf( - DOMRepo( + val testRepo = PagingData.from( + listOf( + DOMRepo( + id = 0, + name = "Test", + fullName = "Test", + description = "Test", + url = "Test", + stars = 0, + forks = 0, + language = "Test", + watchers = 0, + owner = DOMOwner( id = 0, - name = "Test", - fullName = "Test", - description = "Test", - url = "Test", - stars = 0, - forks = 0, - language = "Test", - watchers = 0, - owner = DOMOwner( - id = 0, - login = "Test", - avatarUrl = "Test" - ), - createDate = "Test", - updateDate = "Test", - openIssues = 0 - ) + login = "Test", + avatarUrl = "Test" + ), + createDate = "Test", + updateDate = "Test", + openIssues = 0 ) ) ) coEvery { githubRepoImpl.getTrendingRepos("flutter") - } returns testRepo + } returns flowOf(testRepo) githubTrendingReposUseCase = GetGithubTrendingReposUseCase(githubRepoImpl) @@ -96,9 +98,11 @@ class GithubReposVMTest { ) githubReposVM.getGitHubTrendingRepos() + delay(1) // Because we need our liveData to be assigned first before testing - githubReposVM.reposFlowLiveData.asFlow().test { + githubReposVM.reposFlowLiveData.getOrAwaitValue().test { assert(awaitItem() == testRepo) + cancelAndConsumeRemainingEvents() } } } diff --git a/ui-githubrepos/src/test/java/com/mutualmobile/feat/githubrepos/utils/LiveDataUtil.kt b/ui-githubrepos/src/test/java/com/mutualmobile/feat/githubrepos/utils/LiveDataUtil.kt new file mode 100644 index 00000000..c7b7941d --- /dev/null +++ b/ui-githubrepos/src/test/java/com/mutualmobile/feat/githubrepos/utils/LiveDataUtil.kt @@ -0,0 +1,63 @@ +package com.mutualmobile.feat.githubrepos.utils + +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/** + * Gets the value of a [LiveData] or waits for it to have one, with a timeout. + * + * Use this extension from host-side (JVM) tests. It's recommended to use it alongside + * `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously. + */ +@VisibleForTesting(otherwise = VisibleForTesting.NONE) +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS, + afterObserve: () -> Unit = {} +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + this.observeForever(observer) + + try { + afterObserve.invoke() + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") + } + + } finally { + this.removeObserver(observer) + } + + @Suppress("UNCHECKED_CAST") + return data as T +} \ No newline at end of file