diff --git a/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt b/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt index e4f725c..09f8e8c 100644 --- a/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt +++ b/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt @@ -1,7 +1,10 @@ package org.fnives.test.showcase.testutils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.fnives.test.showcase.storage.database.DatabaseInitialization @@ -15,12 +18,12 @@ import org.junit.jupiter.api.extension.ExtensionContext * * One can access the test dispatcher via [testDispatcher] static getter. */ +@OptIn(ExperimentalCoroutinesApi::class) class TestMainDispatcher : BeforeEachCallback, AfterEachCallback { override fun beforeEach(context: ExtensionContext?) { - val testDispatcher = TestCoroutineDispatcher() + val testDispatcher = StandardTestDispatcher(TestCoroutineScheduler()) privateTestDispatcher = testDispatcher - testDispatcher.pauseDispatcher() DatabaseInitialization.dispatcher = testDispatcher Dispatchers.setMain(testDispatcher) } @@ -31,8 +34,9 @@ class TestMainDispatcher : BeforeEachCallback, AfterEachCallback { } companion object { - private var privateTestDispatcher: TestCoroutineDispatcher? = null - val testDispatcher: TestCoroutineDispatcher - get() = privateTestDispatcher ?: throw IllegalStateException("TestMainDispatcher is in afterEach State") + private var privateTestDispatcher: TestDispatcher? = null + val testDispatcher: TestDispatcher + get() = privateTestDispatcher + ?: throw IllegalStateException("TestMainDispatcher is in afterEach State") } } diff --git a/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt b/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt index aa690d6..4af66dd 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt @@ -1,6 +1,7 @@ package org.fnives.test.showcase.ui.auth import com.jraska.livedata.test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import org.fnives.test.showcase.core.login.LoginUseCase import org.fnives.test.showcase.model.auth.LoginCredentials @@ -27,11 +28,12 @@ import java.util.stream.Stream @Suppress("TestFunctionName") @ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@OptIn(ExperimentalCoroutinesApi::class) internal class AuthViewModelTest { private lateinit var sut: AuthViewModel private lateinit var mockLoginUseCase: LoginUseCase - private val testDispatcher get() = TestMainDispatcher.testDispatcher + private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { @@ -42,7 +44,7 @@ internal class AuthViewModelTest { @DisplayName("GIVEN initialized viewModel WHEN observed THEN loading false other fields are empty") @Test fun initialSetup() { - testDispatcher.resumeDispatcher() + testScheduler.advanceUntilIdle() sut.username.test().assertNoValue() sut.password.test().assertNoValue() @@ -54,11 +56,11 @@ internal class AuthViewModelTest { @DisplayName("GIVEN password text WHEN onPasswordChanged is called THEN password livedata is updated") @Test fun whenPasswordChangedLiveDataIsUpdated() { - testDispatcher.resumeDispatcher() val passwordTestObserver = sut.password.test() sut.onPasswordChanged("a") sut.onPasswordChanged("al") + testScheduler.advanceUntilIdle() passwordTestObserver.assertValueHistory("a", "al") sut.username.test().assertNoValue() @@ -70,11 +72,11 @@ internal class AuthViewModelTest { @DisplayName("GIVEN username text WHEN onUsernameChanged is called THEN username livedata is updated") @Test fun whenUsernameChangedLiveDataIsUpdated() { - testDispatcher.resumeDispatcher() val usernameTestObserver = sut.username.test() sut.onUsernameChanged("a") sut.onUsernameChanged("al") + testScheduler.advanceUntilIdle() usernameTestObserver.assertValueHistory("a", "al") sut.password.test().assertNoValue() @@ -92,7 +94,7 @@ internal class AuthViewModelTest { } sut.onLogin() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() loadingTestObserver.assertValueHistory(false, true, false) runBlocking { verify(mockLoginUseCase, times(1)).invoke(LoginCredentials("", "")) } @@ -106,7 +108,7 @@ internal class AuthViewModelTest { sut.onLogin() sut.onLogin() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() runBlocking { verify(mockLoginUseCase, times(1)).invoke(LoginCredentials("", "")) } verifyNoMoreInteractions(mockLoginUseCase) @@ -122,7 +124,7 @@ internal class AuthViewModelTest { sut.onUsernameChanged("usr") sut.onLogin() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() runBlocking { verify(mockLoginUseCase, times(1)).invoke(LoginCredentials("usr", "pass")) @@ -141,7 +143,7 @@ internal class AuthViewModelTest { val navigateToHomeObserver = sut.navigateToHome.test() sut.onLogin() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() loadingObserver.assertValueHistory(false, true, false) errorObserver.assertValueHistory(Event(AuthViewModel.ErrorType.GENERAL_NETWORK_ERROR)) @@ -162,7 +164,7 @@ internal class AuthViewModelTest { val navigateToHomeObserver = sut.navigateToHome.test() sut.onLogin() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() loadingObserver.assertValueHistory(false, true, false) errorObserver.assertValueHistory(Event(errorType)) @@ -180,7 +182,7 @@ internal class AuthViewModelTest { val navigateToHomeObserver = sut.navigateToHome.test() sut.onLogin() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() loadingObserver.assertValueHistory(false, true, false) errorObserver.assertNoValue() diff --git a/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt b/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt index bb87c92..244f142 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt @@ -1,6 +1,7 @@ package org.fnives.test.showcase.ui.home import com.jraska.livedata.test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase @@ -29,6 +30,7 @@ import org.mockito.kotlin.whenever @Suppress("TestFunctionName") @ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@OptIn(ExperimentalCoroutinesApi::class) internal class MainViewModelTest { private lateinit var sut: MainViewModel @@ -37,7 +39,7 @@ internal class MainViewModelTest { private lateinit var mockFetchContentUseCase: FetchContentUseCase private lateinit var mockAddContentToFavouriteUseCase: AddContentToFavouriteUseCase private lateinit var mockRemoveContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase - private val testDispatcher get() = TestMainDispatcher.testDispatcher + private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { @@ -46,7 +48,6 @@ internal class MainViewModelTest { mockFetchContentUseCase = mock() mockAddContentToFavouriteUseCase = mock() mockRemoveContentFromFavouritesUseCase = mock() - testDispatcher.pauseDispatcher() sut = MainViewModel( getAllContentUseCase = mockGetAllContentUseCase, logoutUseCase = mockLogoutUseCase, @@ -69,13 +70,16 @@ internal class MainViewModelTest { @Test fun loadingDataShowsInLoadingUIState() { whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Loading())) - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + val errorMessageTestObserver = sut.errorMessage.test() + val contentTestObserver = sut.content.test() + val loadingTestObserver = sut.loading.test() + val navigateToAuthTestObserver = sut.navigateToAuth.test() + testScheduler.advanceUntilIdle() - sut.errorMessage.test().assertValue(false) - sut.content.test().assertNoValue() - sut.loading.test().assertValue(true) - sut.navigateToAuth.test().assertNoValue() + errorMessageTestObserver.assertValue(false) + contentTestObserver.assertNoValue() + loadingTestObserver.assertValue(true) + navigateToAuthTestObserver.assertNoValue() } @DisplayName("GIVEN loading then data WHEN observing content THEN proper states are shown") @@ -85,13 +89,13 @@ internal class MainViewModelTest { val errorMessageTestObserver = sut.errorMessage.test() val contentTestObserver = sut.content.test() val loadingTestObserver = sut.loading.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + val navigateToAuthTestObserver = sut.navigateToAuth.test() + testScheduler.advanceUntilIdle() errorMessageTestObserver.assertValueHistory(false) contentTestObserver.assertValueHistory(listOf()) loadingTestObserver.assertValueHistory(true, false) - sut.navigateToAuth.test().assertNoValue() + navigateToAuthTestObserver.assertNoValue() } @DisplayName("GIVEN loading then error WHEN observing content THEN proper states are shown") @@ -101,13 +105,13 @@ internal class MainViewModelTest { val errorMessageTestObserver = sut.errorMessage.test() val contentTestObserver = sut.content.test() val loadingTestObserver = sut.loading.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + val navigateToAuthTestObserver = sut.navigateToAuth.test() + testScheduler.advanceUntilIdle() errorMessageTestObserver.assertValueHistory(false, true) contentTestObserver.assertValueHistory(emptyList()) loadingTestObserver.assertValueHistory(true, false) - sut.navigateToAuth.test().assertNoValue() + navigateToAuthTestObserver.assertNoValue() } @DisplayName("GIVEN loading then error then loading then data WHEN observing content THEN proper states are shown") @@ -127,13 +131,13 @@ internal class MainViewModelTest { val errorMessageTestObserver = sut.errorMessage.test() val contentTestObserver = sut.content.test() val loadingTestObserver = sut.loading.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + val navigateToAuthTestObserver = sut.navigateToAuth.test() + testScheduler.advanceUntilIdle() errorMessageTestObserver.assertValueHistory(false, true, false) contentTestObserver.assertValueHistory(emptyList(), content) loadingTestObserver.assertValueHistory(true, false, true, false) - sut.navigateToAuth.test().assertNoValue() + navigateToAuthTestObserver.assertNoValue() } @DisplayName("GIVEN loading viewModel WHEN refreshing THEN usecase is not called") @@ -141,11 +145,10 @@ internal class MainViewModelTest { fun fetchIsIgnoredIfViewModelIsStillLoading() { whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Loading())) sut.content.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() sut.onRefresh() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() verifyZeroInteractions(mockFetchContentUseCase) } @@ -155,11 +158,10 @@ internal class MainViewModelTest { fun fetchIsCalledIfViewModelIsLoaded() { whenever(mockGetAllContentUseCase.get()).doReturn(flowOf()) sut.content.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() sut.onRefresh() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() verify(mockFetchContentUseCase, times(1)).invoke() verifyNoMoreInteractions(mockFetchContentUseCase) @@ -170,11 +172,10 @@ internal class MainViewModelTest { fun loadingViewModelStillCalsLogout() { whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Loading())) sut.content.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() sut.onLogout() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() runBlocking { verify(mockLogoutUseCase, times(1)).invoke() } verifyNoMoreInteractions(mockLogoutUseCase) @@ -185,11 +186,10 @@ internal class MainViewModelTest { fun nonLoadingViewModelStillCalsLogout() { whenever(mockGetAllContentUseCase.get()).doReturn(flowOf()) sut.content.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() sut.onLogout() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() runBlocking { verify(mockLogoutUseCase, times(1)).invoke() } verifyNoMoreInteractions(mockLogoutUseCase) @@ -204,11 +204,10 @@ internal class MainViewModelTest { ) whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Success(contents))) sut.content.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() sut.onFavouriteToggleClicked(ContentId("c")) - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() verifyZeroInteractions(mockRemoveContentFromFavouritesUseCase) verifyZeroInteractions(mockAddContentToFavouriteUseCase) @@ -223,11 +222,10 @@ internal class MainViewModelTest { ) whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Success(contents))) sut.content.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() sut.onFavouriteToggleClicked(ContentId("b")) - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() runBlocking { verify(mockRemoveContentFromFavouritesUseCase, times(1)).invoke(ContentId("b")) } verifyNoMoreInteractions(mockRemoveContentFromFavouritesUseCase) @@ -243,11 +241,10 @@ internal class MainViewModelTest { ) whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Success(contents))) sut.content.test() - testDispatcher.resumeDispatcher() - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() sut.onFavouriteToggleClicked(ContentId("a")) - testDispatcher.advanceUntilIdle() + testScheduler.advanceUntilIdle() verifyZeroInteractions(mockRemoveContentFromFavouritesUseCase) runBlocking { verify(mockAddContentToFavouriteUseCase, times(1)).invoke(ContentId("a")) } diff --git a/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt b/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt index 5ca371e..bd366e1 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt @@ -1,6 +1,7 @@ package org.fnives.test.showcase.ui.splash import com.jraska.livedata.test +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase import org.fnives.test.showcase.testutils.InstantExecutorExtension import org.fnives.test.showcase.testutils.TestMainDispatcher @@ -14,11 +15,12 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@OptIn(ExperimentalCoroutinesApi::class) internal class SplashViewModelTest { private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase private lateinit var sut: SplashViewModel - private val testCoroutineDispatcher get() = TestMainDispatcher.testDispatcher + private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { @@ -30,29 +32,32 @@ internal class SplashViewModelTest { @Test fun loggedOutUserGoesToAuthentication() { whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false) + val navigateToTestObserver = sut.navigateTo.test() - testCoroutineDispatcher.advanceTimeBy(500) + testScheduler.advanceTimeBy(501) - sut.navigateTo.test().assertValue(Event(SplashViewModel.NavigateTo.AUTHENTICATION)) + navigateToTestObserver.assertValueHistory(Event(SplashViewModel.NavigateTo.AUTHENTICATION)) } @DisplayName("GIVEN logged in user WHEN splash started THEN after half a second navigated to home") @Test - fun loggedInUserGoestoHome() { + fun loggedInUserGoesToHome() { whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(true) + val navigateToTestObserver = sut.navigateTo.test() - testCoroutineDispatcher.advanceTimeBy(500) + testScheduler.advanceTimeBy(501) - sut.navigateTo.test().assertValue(Event(SplashViewModel.NavigateTo.HOME)) + navigateToTestObserver.assertValueHistory(Event(SplashViewModel.NavigateTo.HOME)) } @DisplayName("GIVEN not logged in user WHEN splash started THEN before half a second no event is sent") @Test fun withoutEnoughTimeNoNavigationHappens() { whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false) + val navigateToTestObserver = sut.navigateTo.test() - testCoroutineDispatcher.advanceTimeBy(100) + testScheduler.advanceTimeBy(500) - sut.navigateTo.test().assertNoValue() + navigateToTestObserver.assertNoValue() } } diff --git a/app/src/testKoin/java/org/fnives/test/showcase/di/DITest.kt b/app/src/testKoin/java/org/fnives/test/showcase/di/DITest.kt index 1a88d3d..cf595ba 100644 --- a/app/src/testKoin/java/org/fnives/test/showcase/di/DITest.kt +++ b/app/src/testKoin/java/org/fnives/test/showcase/di/DITest.kt @@ -7,7 +7,6 @@ import org.fnives.test.showcase.ui.auth.AuthViewModel import org.fnives.test.showcase.ui.home.MainViewModel import org.fnives.test.showcase.ui.splash.SplashViewModel import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.koin.android.ext.koin.androidContext @@ -28,11 +27,6 @@ class DITest : KoinTest { private val mainViewModel by inject() private val splashViewModel by inject() - @BeforeEach - fun setUp() { - TestMainDispatcher.testDispatcher.pauseDispatcher() - } - @AfterEach fun tearDown() { stopKoin()