diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2b8017b..1a7057d9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jul 14 15:56:55 IST 2020 +#Tue Jan 25 12:20:13 IST 2022 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/ui-githubrepos/build.gradle.kts b/ui-githubrepos/build.gradle.kts index 7f06ca1f..84be6f1f 100644 --- a/ui-githubrepos/build.gradle.kts +++ b/ui-githubrepos/build.gradle.kts @@ -106,4 +106,5 @@ dependencies { testImplementation(TestLib.ROBO_ELECTRIC) testImplementation(TestLib.COROUTINES) testImplementation(TestLib.MOCKK) + testImplementation(TestLib.TURBINE) } \ No newline at end of file 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 new file mode 100644 index 00000000..9b758600 --- /dev/null +++ b/ui-githubrepos/src/test/java/com/mutualmobile/feat/githubrepos/ui/github/repolist/GithubReposVMTest.kt @@ -0,0 +1,109 @@ +package com.mutualmobile.feat.githubrepos.ui.github.repolist + +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 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 +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule + +@ExperimentalCoroutinesApi +class GithubReposVMTest { + + /** + * Important for your test's execution while testing LiveData. + * Fixes error message: "Android looper not mocked" + */ + @get:Rule + var rule: TestRule = InstantTaskExecutorRule() + + lateinit var githubReposVM: GithubReposVM + + @MockK + lateinit var githubRepoImpl: GithubRepoImpl + + lateinit var githubTrendingReposUseCase: GetGithubTrendingReposUseCase + + @MockK + lateinit var uiRepoMapper: UIRepoMapper + + @Before + fun setUp() { + MockKAnnotations.init(this, true) + Dispatchers.setMain(StandardTestDispatcher()) + uiRepoMapper = UIRepoMapper() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `test that trendingGithubRepos() completes without exception`() = runTest { + launch { + 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, + login = "Test", + avatarUrl = "Test" + ), + createDate = "Test", + updateDate = "Test", + openIssues = 0 + ) + ) + ) + + coEvery { + githubRepoImpl.getTrendingRepos("flutter") + } returns flowOf(testRepo) + + githubTrendingReposUseCase = GetGithubTrendingReposUseCase(githubRepoImpl) + + githubReposVM = GithubReposVM( + githubTrendingReposUseCase, + uiRepoMapper + ) + + githubReposVM.getGitHubTrendingRepos() + delay(1) // Because we need our liveData to be assigned first before testing + + githubReposVM.reposFlowLiveData.getOrAwaitValue().test { + assert(awaitItem() == testRepo) + cancelAndConsumeRemainingEvents() + } + } + } +} \ No newline at end of file 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 diff --git a/ui-jokes/build.gradle.kts b/ui-jokes/build.gradle.kts index 49e23bc0..32aa225f 100644 --- a/ui-jokes/build.gradle.kts +++ b/ui-jokes/build.gradle.kts @@ -97,4 +97,5 @@ dependencies { testImplementation(TestLib.ROBO_ELECTRIC) testImplementation(TestLib.COROUTINES) testImplementation(TestLib.MOCKK) + testImplementation(TestLib.TURBINE) } \ No newline at end of file diff --git a/ui-jokes/src/test/java/com/mutualmobile/feat/jokes/ui/joke/home/HomeVMTest.kt b/ui-jokes/src/test/java/com/mutualmobile/feat/jokes/ui/joke/home/HomeVMTest.kt new file mode 100644 index 00000000..94a708d9 --- /dev/null +++ b/ui-jokes/src/test/java/com/mutualmobile/feat/jokes/ui/joke/home/HomeVMTest.kt @@ -0,0 +1,99 @@ +package com.mutualmobile.feat.jokes.ui.joke.home + +import app.cash.turbine.test +import com.mutualmobile.feat.jokes.ui.model.UIJokeMapper +import com.mutualmobile.praxis.data.repository.JokesRepoImpl +import com.mutualmobile.praxis.domain.SafeResult +import com.mutualmobile.praxis.domain.model.DOMJoke +import com.mutualmobile.praxis.domain.model.DOMJokeList +import com.mutualmobile.praxis.domain.usecases.GetFiveRandomJokesUseCase +import com.mutualmobile.praxis.navigator.ComposeNavigator +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.flow.emptyFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class HomeVMTest { + + private val uiJokesMapper = UIJokeMapper() + + @MockK + private lateinit var navigator: ComposeNavigator + private lateinit var getFiveRandomJokesUseCase: GetFiveRandomJokesUseCase + private lateinit var homeVM: HomeVM + + @MockK + private lateinit var jokesRepoImpl: JokesRepoImpl + + @Before + fun setUp() { + MockKAnnotations.init(this, true) + Dispatchers.setMain(StandardTestDispatcher()) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `test that loadJokes() completes on success response`() = runTest { + + // This is mocking + coEvery { + navigator.observeResult(any()) + } returns emptyFlow() + + coEvery { + jokesRepoImpl.getFiveRandomJokes() + } returns SafeResult.Success( + data = DOMJokeList( + type = "success", + DOMJokes = listOf( + DOMJoke(0, "Test Joke") + ) + ) + ) + + getFiveRandomJokesUseCase = GetFiveRandomJokesUseCase(jokesRepoImpl) + + homeVM = HomeVM(getFiveRandomJokesUseCase, uiJokesMapper, navigator) + + homeVM.viewState.test { + assert(awaitItem() is HomeViewState.Loading) + assert(awaitItem() is HomeViewState.ShowJokes) + } + } + + @Test + fun `test that loadJokes() fails on failed response`() = runTest { + + // This is mocking + coEvery { + navigator.observeResult(any()) + } returns emptyFlow() + + coEvery { + jokesRepoImpl.getFiveRandomJokes() + } returns SafeResult.Failure() + + getFiveRandomJokesUseCase = GetFiveRandomJokesUseCase(jokesRepoImpl) + + homeVM = HomeVM(getFiveRandomJokesUseCase, uiJokesMapper, navigator) + + homeVM.viewState.test { + assert(awaitItem() is HomeViewState.Loading) + assert(awaitItem() is HomeViewState.Error) + } + } +} \ No newline at end of file diff --git a/ui-jokes/src/test/java/com/mutualmobile/feat/jokes/ui/joke/jokedetails/JokeDetailVMTest.kt b/ui-jokes/src/test/java/com/mutualmobile/feat/jokes/ui/joke/jokedetails/JokeDetailVMTest.kt new file mode 100644 index 00000000..c09b26e2 --- /dev/null +++ b/ui-jokes/src/test/java/com/mutualmobile/feat/jokes/ui/joke/jokedetails/JokeDetailVMTest.kt @@ -0,0 +1,72 @@ +package com.mutualmobile.feat.jokes.ui.joke.jokedetails + +import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test +import com.mutualmobile.feat.jokes.ui.joke.InMemoryDataTemp +import com.mutualmobile.feat.jokes.ui.model.UIJoke +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.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class JokeDetailVMTest { + + @MockK + lateinit var savedStateHandle: SavedStateHandle + lateinit var jokeDetailVM: JokeDetailVM + + @Before + fun setUp() { + MockKAnnotations.init(this, true) + Dispatchers.setMain(StandardTestDispatcher()) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `test that uiState emits SuccessState on getting a non-null UIJoke`() = runTest { + launch { + coEvery { + savedStateHandle.get(any()) + } returns 0 + + InMemoryDataTemp.jokes = listOf( + UIJoke(0, "TestJoke") + ) + jokeDetailVM = JokeDetailVM(savedStateHandle) + + jokeDetailVM.uiState.test { + assert(awaitItem() is JokeDetailVM.UiState.SuccessState) + } + } + } + + @Test + fun `test that uiState emits LoadingState on getting a null UIJoke`() = runTest { + launch { + coEvery { + savedStateHandle.get(any()) + } returns 0 + + InMemoryDataTemp.jokes = null + jokeDetailVM = JokeDetailVM(savedStateHandle) + + jokeDetailVM.uiState.test { + assert(awaitItem() is JokeDetailVM.UiState.LoadingState) + } + } + } +} \ No newline at end of file