Issue#41 Copy full example into separate module with Hilt Integration

This commit is contained in:
Gergely Hegedus 2022-09-27 17:16:05 +03:00
parent 69e76dc0da
commit 52a99a82fc
229 changed files with 8416 additions and 11 deletions

View file

@ -0,0 +1,216 @@
package org.fnives.test.showcase.hilt.ui.auth
import com.jraska.livedata.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.android.testutil.InstantExecutorExtension
import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher
import org.fnives.test.showcase.hilt.core.login.LoginUseCase
import org.fnives.test.showcase.hilt.ui.shared.Event
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.shared.Answer
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import java.util.stream.Stream
@Suppress("TestFunctionName")
@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class)
internal class AuthViewModelTest {
private lateinit var sut: AuthViewModel
private lateinit var mockLoginUseCase: LoginUseCase
private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach
fun setUp() {
mockLoginUseCase = mock()
sut = AuthViewModel(mockLoginUseCase)
}
@DisplayName("GIVEN initialized viewModel WHEN observed THEN loading false other fields are empty")
@Test
fun initialSetup() {
val usernameTestObserver = sut.username.test()
val passwordTestObserver = sut.password.test()
val loadingTestObserver = sut.loading.test()
val errorTestObserver = sut.error.test()
val navigateToHomeTestObserver = sut.navigateToHome.test()
testScheduler.advanceUntilIdle()
usernameTestObserver.assertNoValue()
passwordTestObserver.assertNoValue()
loadingTestObserver.assertValue(false)
errorTestObserver.assertNoValue()
navigateToHomeTestObserver.assertNoValue()
}
@DisplayName("GIVEN password text WHEN onPasswordChanged is called THEN password livedata is updated")
@Test
fun whenPasswordChangedLiveDataIsUpdated() {
val usernameTestObserver = sut.username.test()
val passwordTestObserver = sut.password.test()
val loadingTestObserver = sut.loading.test()
val errorTestObserver = sut.error.test()
val navigateToHomeTestObserver = sut.navigateToHome.test()
sut.onPasswordChanged("a")
sut.onPasswordChanged("al")
testScheduler.advanceUntilIdle()
usernameTestObserver.assertNoValue()
passwordTestObserver.assertValueHistory("a", "al")
loadingTestObserver.assertValue(false)
errorTestObserver.assertNoValue()
navigateToHomeTestObserver.assertNoValue()
}
@DisplayName("GIVEN username text WHEN onUsernameChanged is called THEN username livedata is updated")
@Test
fun whenUsernameChangedLiveDataIsUpdated() {
val usernameTestObserver = sut.username.test()
val passwordTestObserver = sut.password.test()
val loadingTestObserver = sut.loading.test()
val errorTestObserver = sut.error.test()
val navigateToHomeTestObserver = sut.navigateToHome.test()
sut.onUsernameChanged("bla")
sut.onUsernameChanged("blabla")
testScheduler.advanceUntilIdle()
usernameTestObserver.assertValueHistory("bla", "blabla")
passwordTestObserver.assertNoValue()
loadingTestObserver.assertValue(false)
errorTestObserver.assertNoValue()
navigateToHomeTestObserver.assertNoValue()
}
@DisplayName("GIVEN no password or username WHEN login is Called THEN empty credentials are used in usecase")
@Test
fun noPasswordUsesEmptyStringInLoginUseCase() {
val loadingTestObserver = sut.loading.test()
runBlocking {
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable()))
}
sut.onLogin()
testScheduler.advanceUntilIdle()
loadingTestObserver.assertValueHistory(false, true, false)
runBlocking { verify(mockLoginUseCase, times(1)).invoke(LoginCredentials("", "")) }
verifyNoMoreInteractions(mockLoginUseCase)
}
@DisplayName("WHEN login is called twice before finishing THEN use case is only called once")
@Test
fun onlyOneLoginIsSentOutWhenClickingRepeatedly() {
runBlocking { whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable())) }
sut.onLogin()
sut.onLogin()
testScheduler.advanceUntilIdle()
runBlocking { verify(mockLoginUseCase, times(1)).invoke(LoginCredentials("", "")) }
verifyNoMoreInteractions(mockLoginUseCase)
}
@DisplayName("GIVEN password and username WHEN login is called THEN proper credentials are used in usecase")
@Test
fun argumentsArePassedProperlyToLoginUseCase() {
runBlocking {
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable()))
}
sut.onPasswordChanged("pass")
sut.onUsernameChanged("usr")
testScheduler.advanceUntilIdle()
sut.onLogin()
testScheduler.advanceUntilIdle()
runBlocking {
verify(mockLoginUseCase, times(1)).invoke(LoginCredentials("usr", "pass"))
}
verifyNoMoreInteractions(mockLoginUseCase)
}
@DisplayName("GIVEN AnswerError WHEN login called THEN error is shown")
@Test
fun loginUnexpectedErrorResultsInErrorState() {
runBlocking {
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable()))
}
val loadingTestObserver = sut.loading.test()
val errorTestObserver = sut.error.test()
val navigateToHomeTestObserver = sut.navigateToHome.test()
sut.onLogin()
testScheduler.advanceUntilIdle()
loadingTestObserver.assertValueHistory(false, true, false)
errorTestObserver.assertValueHistory(Event(AuthViewModel.ErrorType.GENERAL_NETWORK_ERROR))
navigateToHomeTestObserver.assertNoValue()
}
@MethodSource("loginErrorStatusesArguments")
@ParameterizedTest(name = "GIVEN answer success loginStatus {0} WHEN login called THEN error {1} is shown")
fun invalidStatusResultsInErrorState(
loginStatus: LoginStatus,
errorType: AuthViewModel.ErrorType,
) {
runBlocking {
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Success(loginStatus))
}
val loadingTestObserver = sut.loading.test()
val errorTestObserver = sut.error.test()
val navigateToHomeTestObserver = sut.navigateToHome.test()
sut.onLogin()
testScheduler.advanceUntilIdle()
loadingTestObserver.assertValueHistory(false, true, false)
errorTestObserver.assertValueHistory(Event(errorType))
navigateToHomeTestObserver.assertNoValue()
}
@DisplayName("GIVEN answer success and login status success WHEN login called THEN navigation event is sent")
@Test
fun successLoginResultsInNavigation() {
runBlocking {
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Success(LoginStatus.SUCCESS))
}
val loadingTestObserver = sut.loading.test()
val errorTestObserver = sut.error.test()
val navigateToHomeTestObserver = sut.navigateToHome.test()
sut.onLogin()
testScheduler.advanceUntilIdle()
loadingTestObserver.assertValueHistory(false, true, false)
errorTestObserver.assertNoValue()
navigateToHomeTestObserver.assertValueHistory(Event(Unit))
}
companion object {
@JvmStatic
fun loginErrorStatusesArguments(): Stream<Arguments?> = Stream.of(
Arguments.of(LoginStatus.INVALID_CREDENTIALS, AuthViewModel.ErrorType.INVALID_CREDENTIALS),
Arguments.of(LoginStatus.INVALID_PASSWORD, AuthViewModel.ErrorType.UNSUPPORTED_PASSWORD),
Arguments.of(LoginStatus.INVALID_USERNAME, AuthViewModel.ErrorType.UNSUPPORTED_USERNAME)
)
}
}

View file

@ -0,0 +1,253 @@
package org.fnives.test.showcase.hilt.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.android.testutil.InstantExecutorExtension
import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher
import org.fnives.test.showcase.hilt.core.content.AddContentToFavouriteUseCase
import org.fnives.test.showcase.hilt.core.content.FetchContentUseCase
import org.fnives.test.showcase.hilt.core.content.GetAllContentUseCase
import org.fnives.test.showcase.hilt.core.content.RemoveContentFromFavouritesUseCase
import org.fnives.test.showcase.hilt.core.login.LogoutUseCase
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.model.shared.Resource
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mockito.verifyNoInteractions
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class)
internal class MainViewModelTest {
private lateinit var sut: MainViewModel
private lateinit var mockGetAllContentUseCase: GetAllContentUseCase
private lateinit var mockLogoutUseCase: LogoutUseCase
private lateinit var mockFetchContentUseCase: FetchContentUseCase
private lateinit var mockAddContentToFavouriteUseCase: AddContentToFavouriteUseCase
private lateinit var mockRemoveContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase
private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach
fun setUp() {
mockGetAllContentUseCase = mock()
mockLogoutUseCase = mock()
mockFetchContentUseCase = mock()
mockAddContentToFavouriteUseCase = mock()
mockRemoveContentFromFavouritesUseCase = mock()
sut = MainViewModel(
getAllContentUseCase = mockGetAllContentUseCase,
logoutUseCase = mockLogoutUseCase,
fetchContentUseCase = mockFetchContentUseCase,
addContentToFavouriteUseCase = mockAddContentToFavouriteUseCase,
removeContentFromFavouritesUseCase = mockRemoveContentFromFavouritesUseCase
)
}
@DisplayName("WHEN initialization THEN error false other states empty")
@Test
fun initialStateIsCorrect() {
sut.errorMessage.test().assertValue(false)
sut.content.test().assertNoValue()
sut.loading.test().assertNoValue()
sut.navigateToAuth.test().assertNoValue()
}
@DisplayName("GIVEN initialized viewModel WHEN loading is returned THEN loading is shown")
@Test
fun loadingDataShowsInLoadingUIState() {
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Loading()))
val errorMessageTestObserver = sut.errorMessage.test()
val contentTestObserver = sut.content.test()
val loadingTestObserver = sut.loading.test()
val navigateToAuthTestObserver = sut.navigateToAuth.test()
testScheduler.advanceUntilIdle()
errorMessageTestObserver.assertValue(false)
contentTestObserver.assertNoValue()
loadingTestObserver.assertValue(true)
navigateToAuthTestObserver.assertNoValue()
}
@DisplayName("GIVEN loading then data WHEN observing content THEN proper states are shown")
@Test
fun loadingThenLoadedDataResultsInProperUIStates() {
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Loading(), Resource.Success(emptyList())))
val errorMessageTestObserver = sut.errorMessage.test()
val contentTestObserver = sut.content.test()
val loadingTestObserver = sut.loading.test()
val navigateToAuthTestObserver = sut.navigateToAuth.test()
testScheduler.advanceUntilIdle()
errorMessageTestObserver.assertValueHistory(false)
contentTestObserver.assertValueHistory(listOf())
loadingTestObserver.assertValueHistory(true, false)
navigateToAuthTestObserver.assertNoValue()
}
@DisplayName("GIVEN loading then error WHEN observing content THEN proper states are shown")
@Test
fun loadingThenErrorResultsInProperUIStates() {
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Loading(), Resource.Error(Throwable())))
val errorMessageTestObserver = sut.errorMessage.test()
val contentTestObserver = sut.content.test()
val loadingTestObserver = sut.loading.test()
val navigateToAuthTestObserver = sut.navigateToAuth.test()
testScheduler.advanceUntilIdle()
errorMessageTestObserver.assertValueHistory(false, true)
contentTestObserver.assertValueHistory(emptyList())
loadingTestObserver.assertValueHistory(true, false)
navigateToAuthTestObserver.assertNoValue()
}
@DisplayName("GIVEN loading then error then loading then data WHEN observing content THEN proper states are shown")
@Test
fun loadingThenErrorThenLoadingThenDataResultsInProperUIStates() {
val content = listOf(
FavouriteContent(Content(ContentId(""), "", "", ImageUrl("")), false)
)
whenever(mockGetAllContentUseCase.get()).doReturn(
flowOf(
Resource.Loading(),
Resource.Error(Throwable()),
Resource.Loading(),
Resource.Success(content)
)
)
val errorMessageTestObserver = sut.errorMessage.test()
val contentTestObserver = sut.content.test()
val loadingTestObserver = sut.loading.test()
val navigateToAuthTestObserver = sut.navigateToAuth.test()
testScheduler.advanceUntilIdle()
errorMessageTestObserver.assertValueHistory(false, true, false)
contentTestObserver.assertValueHistory(emptyList(), content)
loadingTestObserver.assertValueHistory(true, false, true, false)
navigateToAuthTestObserver.assertNoValue()
}
@DisplayName("GIVEN loading viewModel WHEN refreshing THEN usecase is not called")
@Test
fun fetchIsIgnoredIfViewModelIsStillLoading() {
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Loading()))
sut.content.test()
testScheduler.advanceUntilIdle()
sut.onRefresh()
testScheduler.advanceUntilIdle()
verifyNoInteractions(mockFetchContentUseCase)
}
@DisplayName("GIVEN non loading viewModel WHEN refreshing THEN usecase is called")
@Test
fun fetchIsCalledIfViewModelIsLoaded() {
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf())
sut.content.test()
testScheduler.advanceUntilIdle()
sut.onRefresh()
testScheduler.advanceUntilIdle()
verify(mockFetchContentUseCase, times(1)).invoke()
verifyNoMoreInteractions(mockFetchContentUseCase)
}
@DisplayName("GIVEN loading viewModel WHEN loging out THEN usecase is called")
@Test
fun loadingViewModelStillCalsLogout() {
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Loading()))
sut.content.test()
testScheduler.advanceUntilIdle()
sut.onLogout()
testScheduler.advanceUntilIdle()
runBlocking { verify(mockLogoutUseCase, times(1)).invoke() }
verifyNoMoreInteractions(mockLogoutUseCase)
}
@DisplayName("GIVEN non loading viewModel WHEN loging out THEN usecase is called")
@Test
fun nonLoadingViewModelStillCalsLogout() {
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf())
sut.content.test()
testScheduler.advanceUntilIdle()
sut.onLogout()
testScheduler.advanceUntilIdle()
runBlocking { verify(mockLogoutUseCase, times(1)).invoke() }
verifyNoMoreInteractions(mockLogoutUseCase)
}
@DisplayName("GIVEN success content list viewModel WHEN toggling a nonexistent contentId THEN nothing happens")
@Test
fun interactionWithNonExistentContentIdIsIgnored() {
val contents = listOf(
FavouriteContent(Content(ContentId("a"), "", "", ImageUrl("")), false),
FavouriteContent(Content(ContentId("b"), "", "", ImageUrl("")), true)
)
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Success(contents)))
sut.content.test()
testScheduler.advanceUntilIdle()
sut.onFavouriteToggleClicked(ContentId("c"))
testScheduler.advanceUntilIdle()
verifyNoInteractions(mockRemoveContentFromFavouritesUseCase)
verifyNoInteractions(mockAddContentToFavouriteUseCase)
}
@DisplayName("GIVEN success content list viewModel WHEN toggling a favourite contentId THEN remove favourite usecase is called")
@Test
fun togglingFavouriteContentCallsRemoveFromFavourite() {
val contents = listOf(
FavouriteContent(Content(ContentId("a"), "", "", ImageUrl("")), false),
FavouriteContent(Content(ContentId("b"), "", "", ImageUrl("")), true)
)
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Success(contents)))
sut.content.test()
testScheduler.advanceUntilIdle()
sut.onFavouriteToggleClicked(ContentId("b"))
testScheduler.advanceUntilIdle()
runBlocking { verify(mockRemoveContentFromFavouritesUseCase, times(1)).invoke(ContentId("b")) }
verifyNoMoreInteractions(mockRemoveContentFromFavouritesUseCase)
verifyNoInteractions(mockAddContentToFavouriteUseCase)
}
@DisplayName("GIVEN success content list viewModel WHEN toggling a not favourite contentId THEN add favourite usecase is called")
@Test
fun togglingNonFavouriteContentCallsAddToFavourite() {
val contents = listOf(
FavouriteContent(Content(ContentId("a"), "", "", ImageUrl("")), false),
FavouriteContent(Content(ContentId("b"), "", "", ImageUrl("")), true)
)
whenever(mockGetAllContentUseCase.get()).doReturn(flowOf(Resource.Success(contents)))
sut.content.test()
testScheduler.advanceUntilIdle()
sut.onFavouriteToggleClicked(ContentId("a"))
testScheduler.advanceUntilIdle()
verifyNoInteractions(mockRemoveContentFromFavouritesUseCase)
runBlocking { verify(mockAddContentToFavouriteUseCase, times(1)).invoke(ContentId("a")) }
verifyNoMoreInteractions(mockAddContentToFavouriteUseCase)
}
}

View file

@ -0,0 +1,53 @@
package org.fnives.test.showcase.hilt.ui.shared
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@Suppress("TestFunctionName")
internal class EventTest {
@DisplayName("GIVEN event WHEN consumed is called THEN value is returned")
@Test
fun consumedReturnsValue() {
val expected = "a"
val actual = Event("a").consume()
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN consumed event WHEN consumed is called THEN null is returned")
@Test
fun consumedEventReturnsNull() {
val expected: String? = null
val event = Event("a")
event.consume()
val actual = event.consume()
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN event WHEN peek is called THEN value is returned")
@Test
fun peekReturnsValue() {
val expected = "a"
val actual = Event("a").peek()
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN consumed event WHEN peek is called THEN value is returned")
@Test
fun consumedEventPeekedReturnsValue() {
val expected = "a"
val event = Event("a")
event.consume()
val actual = event.peek()
Assertions.assertEquals(expected, actual)
}
}

View file

@ -0,0 +1,63 @@
package org.fnives.test.showcase.hilt.ui.splash
import com.jraska.livedata.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.fnives.test.showcase.android.testutil.InstantExecutorExtension
import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher
import org.fnives.test.showcase.hilt.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.hilt.ui.shared.Event
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class)
internal class SplashViewModelTest {
private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase
private lateinit var sut: SplashViewModel
private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach
fun setUp() {
mockIsUserLoggedInUseCase = mock()
sut = SplashViewModel(mockIsUserLoggedInUseCase)
}
@DisplayName("GIVEN not logged in user WHEN splash started THEN after half a second navigated to authentication")
@Test
fun loggedOutUserGoesToAuthentication() {
whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false)
val navigateToTestObserver = sut.navigateTo.test()
testScheduler.advanceTimeBy(501)
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() {
whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(true)
val navigateToTestObserver = sut.navigateTo.test()
testScheduler.advanceTimeBy(501)
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()
testScheduler.advanceTimeBy(500)
navigateToTestObserver.assertNoValue()
}
}

View file

@ -0,0 +1 @@
mock-maker-inline

View file

@ -0,0 +1,3 @@
sdk=22,28
instrumentedPackages=androidx.loader.content
application = dagger.hilt.android.testing.HiltTestApplication