Issue#11 Adjust TestMainDispatcher extension using tests after coroutine update

This commit is contained in:
Gergely Hegedus 2022-01-23 21:07:46 +02:00
parent 3c85431d96
commit 46d9263742
5 changed files with 70 additions and 68 deletions

View file

@ -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")
}
}

View file

@ -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()

View file

@ -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")) }

View file

@ -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()
}
}

View file

@ -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<MainViewModel>()
private val splashViewModel by inject<SplashViewModel>()
@BeforeEach
fun setUp() {
TestMainDispatcher.testDispatcher.pauseDispatcher()
}
@AfterEach
fun tearDown() {
stopKoin()