Add Hilt(Dagger) example of android/shared tests

This commit is contained in:
Gergely Hegedus 2021-09-19 02:14:12 +03:00
parent e8d0c746b9
commit e4f42baaed
34 changed files with 840 additions and 91 deletions

View file

@ -32,6 +32,7 @@ android {
hilt {
dimension 'di'
applicationId "org.fnives.test.showcase.hilt"
testInstrumentationRunner "org.fnives.test.showcase.testutils.configuration.HiltTestRunner"
}
koin {
dimension 'di'
@ -47,15 +48,24 @@ android {
androidTest {
java.srcDirs += "src/sharedTest/java"
}
androidTestHilt {
java.srcDirs += "src/sharedTestHilt/java"
}
androidTestKoin {
java.srcDirs += "src/sharedTestKoin/java"
}
test {
java.srcDirs += "src/sharedTest/java"
java.srcDirs += "src/robolectricTest/java"
}
testHilt {
java.srcDirs += "src/sharedTestHilt/java"
java.srcDirs += "src/robolectricTestHilt/java"
resources.srcDirs += "src/robolectricTestHilt/resources"
}
testKoin {
java.srcDirs += "src/sharedTestKoin/java"
java.srcDirs += "src/robolectricTestKoin/java"
}
}
@ -147,4 +157,5 @@ dependencies {
androidTestRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-compiler:$hilt_version"
androidTestImplementation project(":network") // hilt needs it
}

View file

@ -0,0 +1,14 @@
package org.fnives.test.showcase.testutils.configuration
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
object AndroidTestServerTypeConfiguration : ServerTypeConfiguration {
override val useHttps: Boolean get() = true
override val url: String get() = "${MockServerScenarioSetup.HTTPS_BASE_URL}:${MockServerScenarioSetup.PORT}/"
override fun invoke(mockServerScenarioSetup: MockServerScenarioSetup) {
val handshakeCertificates = mockServerScenarioSetup.clientCertificates ?: return
HttpsConfigurationModule.handshakeCertificates = handshakeCertificates
}
}

View file

@ -0,0 +1,12 @@
package org.fnives.test.showcase.testutils.configuration
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application =
super.newApplication(cl, HiltTestApplication::class.java.name, context)
}

View file

@ -0,0 +1,33 @@
package org.fnives.test.showcase.testutils.configuration
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import okhttp3.tls.HandshakeCertificates
import org.fnives.test.showcase.hilt.SessionLessQualifier
import org.fnives.test.showcase.network.di.hilt.BindsBaseOkHttpClient
import org.fnives.test.showcase.network.di.hilt.HiltNetworkModule
import javax.inject.Singleton
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [BindsBaseOkHttpClient::class]
)
object HttpsConfigurationModule {
lateinit var handshakeCertificates: HandshakeCertificates
@Provides
@Singleton
@SessionLessQualifier
fun bindsBaseOkHttpClient(enableLogging: Boolean) =
HiltNetworkModule.provideSessionLessOkHttpClient(enableLogging)
.newBuilder()
.sslSocketFactory(
handshakeCertificates.sslSocketFactory(),
handshakeCertificates.trustManager
)
.build()
}

View file

@ -1,29 +0,0 @@
package org.fnives.test.showcase.testutils
//import org.fnives.test.showcase.di.createAppModules
//import org.koin.android.ext.koin.androidContext
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
//import org.koin.core.context.GlobalContext
//import org.koin.core.context.startKoin
class ReloadKoinModulesIfNecessaryTestRule : TestRule {
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
override fun evaluate() {
// if (GlobalContext.getOrNull() == null) {
// val application = ApplicationProvider.getApplicationContext<TestShowcaseApplication>()
// startKoin {
// androidContext(application)
// modules(createAppModules(BaseUrlProvider.get()))
// }
}
// try {
// base.evaluate()
// } finally {
// stopKoin()
// }
// }
}
}

View file

@ -1,31 +1,31 @@
package org.fnives.test.showcase.testutils.statesetup
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.core.login.LoginUseCase
import org.fnives.test.showcase.core.login.LogoutUseCase
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.koin.test.KoinTest
import org.koin.test.get
object SetupLoggedInState : KoinTest {
private val logoutUseCase get() = get<LogoutUseCase>()
private val loginUseCase get() = get<LoginUseCase>()
private val isUserLoggedInUseCase get() = get<IsUserLoggedInUseCase>()
fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
runBlocking {
loginUseCase.invoke(LoginCredentials("a", "b"))
}
}
fun isLoggedIn() = isUserLoggedInUseCase.invoke()
fun setupLogout() {
runBlocking { logoutUseCase.invoke() }
}
}
//package org.fnives.test.showcase.testutils.statesetup
//
//import kotlinx.coroutines.runBlocking
//import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
//import org.fnives.test.showcase.core.login.LoginUseCase
//import org.fnives.test.showcase.core.login.LogoutUseCase
//import org.fnives.test.showcase.model.auth.LoginCredentials
//import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
//import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
//import org.koin.test.KoinTest
//import org.koin.test.get
//
//object SetupLoggedInState : KoinTest {
//
// private val logoutUseCase get() = get<LogoutUseCase>()
// private val loginUseCase get() = get<LoginUseCase>()
// private val isUserLoggedInUseCase get() = get<IsUserLoggedInUseCase>()
//
// fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
// mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
// runBlocking {
// loginUseCase.invoke(LoginCredentials("a", "b"))
// }
// }
//
// fun isLoggedIn() = isUserLoggedInUseCase.invoke()
//
// fun setupLogout() {
// runBlocking { logoutUseCase.invoke() }
// }
//}

View file

@ -21,14 +21,14 @@ import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.PullToRefresh
import org.fnives.test.showcase.testutils.viewactions.WithDrawable
import org.fnives.test.showcase.testutils.viewactions.notIntended
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.ActivityClassHolder
import org.hamcrest.Matchers.allOf
class HomeRobot : Robot {
override fun init() {
Intents.init()
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null))
}
@ -37,11 +37,11 @@ class HomeRobot : Robot {
}
fun assertNavigatedToAuth() = apply {
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
}
fun assertDidNotNavigateToAuth() = apply {
notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
notIntended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
}
fun clickSignOut() = apply {

View file

@ -21,7 +21,7 @@ import org.fnives.test.showcase.testutils.configuration.SpecificTestConfiguratio
import org.fnives.test.showcase.testutils.configuration.TestConfigurationsFactory
import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.notIntended
import org.fnives.test.showcase.ui.home.MainActivity
import org.fnives.test.showcase.ui.ActivityClassHolder
import org.hamcrest.core.IsNot.not
class LoginRobot(
@ -37,7 +37,7 @@ class LoginRobot(
override fun init() {
Intents.init()
intending(hasComponent(MainActivity::class.java.canonicalName))
intending(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
}
@ -91,10 +91,10 @@ class LoginRobot(
}
fun assertNavigatedToHome() = apply {
intended(hasComponent(MainActivity::class.java.canonicalName))
intended(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
}
fun assertNotNavigatedToHome() = apply {
notIntended(hasComponent(MainActivity::class.java.canonicalName))
notIntended(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
}
}

View file

@ -5,16 +5,15 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.notIntended
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.home.MainActivity
import org.fnives.test.showcase.ui.ActivityClassHolder
class SplashRobot : Robot {
override fun init() {
Intents.init()
Intents.intending(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null))
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null))
}
@ -23,18 +22,18 @@ class SplashRobot : Robot {
}
fun assertHomeIsStarted() = apply {
Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
}
fun assertHomeIsNotStarted() = apply {
notIntended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
notIntended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
}
fun assertAuthIsStarted() = apply {
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
}
fun assertAuthIsNotStarted() = apply {
notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
notIntended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
}
}

View file

@ -0,0 +1,42 @@
package org.fnives.test.showcase.testutils.idling
import androidx.annotation.CheckResult
import androidx.test.espresso.IdlingResource
import com.jakewharton.espresso.OkHttp3IdlingResource
import okhttp3.OkHttpClient
import org.fnives.test.showcase.hilt.SessionLessQualifier
import org.fnives.test.showcase.hilt.SessionQualifier
import javax.inject.Inject
class NetworkSynchronization @Inject constructor(
@SessionQualifier
private val sessionOkhttpClient: OkHttpClient,
@SessionLessQualifier
private val sessionlessOkhttpClient: OkHttpClient
) {
@CheckResult
fun registerNetworkingSynchronization(): Disposable {
val idlingResources = OkHttpClientTypes.values()
.map { it to getOkHttpClient(it) }
.associateBy { it.second.dispatcher }
.values
.map { (key, client) -> client.asIdlingResource(key.qualifier) }
.map(::IdlingResourceDisposable)
return CompositeDisposable(idlingResources)
}
private fun getOkHttpClient(type: OkHttpClientTypes): OkHttpClient =
when (type) {
OkHttpClientTypes.SESSION -> sessionOkhttpClient
OkHttpClientTypes.SESSIONLESS -> sessionlessOkhttpClient
}
private fun OkHttpClient.asIdlingResource(name: String): IdlingResource =
OkHttp3IdlingResource.create(name, this)
enum class OkHttpClientTypes(val qualifier: String) {
SESSION("SESSION-NETWORKING"), SESSIONLESS("SESSIONLESS-NETWORKING")
}
}

View file

@ -0,0 +1,30 @@
package org.fnives.test.showcase.testutils.statesetup
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.core.login.LoginUseCase
import org.fnives.test.showcase.core.login.LogoutUseCase
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import javax.inject.Inject
class SetupLoggedInState @Inject constructor(
private val logoutUseCase : LogoutUseCase,
private val loginUseCase : LoginUseCase,
private val isUserLoggedInUseCase: IsUserLoggedInUseCase
) {
fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
runBlocking {
loginUseCase.invoke(LoginCredentials("a", "b"))
}
}
fun isLoggedIn() = isUserLoggedInUseCase.invoke()
fun setupLogout() {
runBlocking { logoutUseCase.invoke() }
}
}

View file

@ -0,0 +1,14 @@
package org.fnives.test.showcase.ui
import org.fnives.test.showcase.ui.auth.HiltAuthActivity
import org.fnives.test.showcase.ui.home.HiltMainActivity
import org.fnives.test.showcase.ui.splash.HiltSplashActivity
object ActivityClassHolder {
fun authActivity() = HiltAuthActivity::class
fun mainActivity() = HiltMainActivity::class
fun splashActivity() = HiltSplashActivity::class
}

View file

@ -0,0 +1,248 @@
package org.fnives.test.showcase.ui.home
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule
import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory
import org.fnives.test.showcase.testutils.idling.Disposable
import org.fnives.test.showcase.testutils.idling.NetworkSynchronization
import org.fnives.test.showcase.testutils.idling.loopMainThreadFor
import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources
import org.fnives.test.showcase.testutils.robot.RobotTestRule
import org.fnives.test.showcase.testutils.statesetup.SetupLoggedInState
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
@Suppress("TestFunctionName")
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class MainActivityTest {
private lateinit var activityScenario: ActivityScenario<HiltMainActivity>
@Rule
@JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Rule
@JvmField
val snackbarVerificationTestRule = SpecificTestConfigurationsFactory.createSnackbarVerification()
@Rule
@JvmField
val robotRule = RobotTestRule(HomeRobot())
private val homeRobot get() = robotRule.robot
@Rule
@JvmField
val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
@Rule
@JvmField
val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule()
@Rule
@JvmField
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var setupLoggedInState: SetupLoggedInState
@Inject
lateinit var networkSynchronization: NetworkSynchronization
private lateinit var disposable: Disposable
@Before
fun setUp() {
SpecificTestConfigurationsFactory.createServerTypeConfiguration()
.invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
hiltRule.inject()
setupLoggedInState.setupLogin(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
disposable = networkSynchronization.registerNetworkingSynchronization()
}
@After
fun tearDown() {
activityScenario.moveToState(Lifecycle.State.DESTROYED)
disposable.dispose()
}
@Test
fun GIVEN_initialized_MainActivity_WHEN_signout_is_clicked_THEN_user_is_signed_out() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Error(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickSignOut()
mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed()
homeRobot.assertNavigatedToAuth()
Assert.assertEquals(false, setupLoggedInState.isLoggedIn())
}
@Test
fun GIVEN_success_response_WHEN_data_is_returned_THEN_it_is_shown_on_the_ui() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
ContentData.contentSuccess.forEach {
homeRobot.assertContainsItem(FavouriteContent(it, false))
}
homeRobot.assertDidNotNavigateToAuth()
}
@Test
fun GIVEN_success_response_WHEN_item_is_clicked_THEN_ui_is_updated() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickOnContentItem(ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
homeRobot.assertContainsItem(expectedItem)
.assertDidNotNavigateToAuth()
}
@Test
fun GIVEN_success_response_WHEN_item_is_clicked_THEN_ui_is_updated_even_if_activity_is_recreated() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickOnContentItem(ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
activityScenario.moveToState(Lifecycle.State.DESTROYED)
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.assertContainsItem(expectedItem)
.assertDidNotNavigateToAuth()
}
@Test
fun GIVEN_success_response_WHEN_item_is_clicked_then_clicked_again_THEN_ui_is_updated() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickOnContentItem(ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickOnContentItem(ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), false)
homeRobot.assertContainsItem(expectedItem)
.assertDidNotNavigateToAuth()
}
@Test
fun GIVEN_error_response_WHEN_loaded_THEN_error_is_Shown() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Error(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.assertContainsNoItems()
.assertContainsError()
.assertDidNotNavigateToAuth()
}
@Test
fun GIVEN_error_response_then_success_WHEN_retried_THEN_success_is_shown() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(
ContentScenario.Error(false)
.then(ContentScenario.Success(false))
)
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.swipeRefresh()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loopMainThreadFor(2000L)
ContentData.contentSuccess.forEach {
homeRobot.assertContainsItem(FavouriteContent(it, false))
}
homeRobot.assertDidNotNavigateToAuth()
}
@Test
fun GIVEN_success_then_error_WHEN_retried_THEN_error_is_shown() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(
ContentScenario.Success(false)
.then(ContentScenario.Error(false))
)
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.swipeRefresh()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loopMainThreadUntilIdleWithIdlingResources()
mainDispatcherTestRule.advanceTimeBy(1000L)
loopMainThreadFor(1000)
homeRobot
.assertContainsError()
.assertContainsNoItems()
.assertDidNotNavigateToAuth()
}
@Test
fun GIVEN_unauthenticated_then_success_WHEN_loaded_THEN_success_is_shown() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(
ContentScenario.Unauthorized(false)
.then(ContentScenario.Success(true))
)
.setScenario(RefreshTokenScenario.Success)
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
ContentData.contentSuccess.forEach {
homeRobot.assertContainsItem(FavouriteContent(it, false))
}
homeRobot.assertDidNotNavigateToAuth()
}
@Test
fun GIVEN_unauthenticated_then_error_WHEN_loaded_THEN_navigated_to_auth() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Unauthorized(false))
.setScenario(RefreshTokenScenario.Error)
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.assertNavigatedToAuth()
Assert.assertEquals(false, setupLoggedInState.isLoggedIn())
}
}

View file

@ -0,0 +1,165 @@
package org.fnives.test.showcase.ui.login
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.fnives.test.showcase.R
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule
import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory
import org.fnives.test.showcase.testutils.idling.Disposable
import org.fnives.test.showcase.testutils.idling.NetworkSynchronization
import org.fnives.test.showcase.testutils.robot.RobotTestRule
import org.fnives.test.showcase.ui.auth.HiltAuthActivity
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
@Suppress("TestFunctionName")
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class AuthActivityTest {
private lateinit var activityScenario: ActivityScenario<HiltAuthActivity>
@Rule
@JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Rule
@JvmField
val snackbarVerificationTestRule = SpecificTestConfigurationsFactory.createSnackbarVerification()
@Rule
@JvmField
val robotRule = RobotTestRule(LoginRobot())
private val loginRobot get() = robotRule.robot
@Rule
@JvmField
val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
@Rule
@JvmField
val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule()
@Rule
@JvmField
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var networkSynchronization: NetworkSynchronization
private lateinit var disposable: Disposable
@Before
fun setUp() {
SpecificTestConfigurationsFactory.createServerTypeConfiguration()
.invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
hiltRule.inject()
disposable = networkSynchronization.registerNetworkingSynchronization()
}
@After
fun tearDown() {
activityScenario.moveToState(Lifecycle.State.DESTROYED)
disposable.dispose()
}
@Test
fun GIVEN_non_empty_password_and_username_and_successful_response_WHEN_signIn_THEN_no_error_is_shown_and_navigating_to_home() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup.setScenario(
AuthScenario.Success(
password = "alma",
username = "banan"
)
)
activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
loginRobot
.setPassword("alma")
.setUsername("banan")
.assertPassword("alma")
.assertUsername("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed()
loginRobot.assertNavigatedToHome()
}
@Test
fun GIVEN_empty_password_and_username_WHEN_signIn_THEN_error_password_is_shown() {
activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
loginRobot
.setUsername("banan")
.assertUsername("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loginRobot.assertErrorIsShown(R.string.password_is_invalid)
.assertNotNavigatedToHome()
.assertNotLoading()
}
@Test
fun GIVEN_password_and_empty_username_WHEN_signIn_THEN_error_username_is_shown() {
activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
loginRobot
.setPassword("banan")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loginRobot.assertErrorIsShown(R.string.username_is_invalid)
.assertNotNavigatedToHome()
.assertNotLoading()
}
@Test
fun GIVEN_password_and_username_and_invalid_credentials_response_WHEN_signIn_THEN_error_invalid_credentials_is_shown() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup.setScenario(
AuthScenario.InvalidCredentials(username = "alma", password = "banan")
)
activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
loginRobot
.setUsername("alma")
.setPassword("banan")
.assertUsername("alma")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loginRobot.assertErrorIsShown(R.string.credentials_invalid)
.assertNotNavigatedToHome()
.assertNotLoading()
}
@Test
fun GIVEN_password_and_username_and_error_response_WHEN_signIn_THEN_error_invalid_credentials_is_shown() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup.setScenario(
AuthScenario.GenericError(username = "alma", password = "banan")
)
activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
loginRobot
.setUsername("alma")
.setPassword("banan")
.assertUsername("alma")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loginRobot.assertErrorIsShown(R.string.something_went_wrong)
.assertNotNavigatedToHome()
.assertNotLoading()
}
}

View file

@ -0,0 +1,99 @@
package org.fnives.test.showcase.ui.splash
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule
import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory
import org.fnives.test.showcase.testutils.idling.Disposable
import org.fnives.test.showcase.testutils.idling.NetworkSynchronization
import org.fnives.test.showcase.testutils.robot.RobotTestRule
import org.fnives.test.showcase.testutils.statesetup.SetupLoggedInState
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.test.KoinTest
import javax.inject.Inject
@Suppress("TestFunctionName")
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class SplashActivityTest : KoinTest {
private var activityScenario: ActivityScenario<HiltSplashActivity>? = null
private val splashRobot: SplashRobot get() = robotTestRule.robot
@Rule
@JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Rule
@JvmField
val robotTestRule = RobotTestRule(SplashRobot())
@Rule
@JvmField
val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule()
@Rule
@JvmField
val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
@Rule
@JvmField
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var setupLoggedInState: SetupLoggedInState
@Inject
lateinit var networkSynchronization: NetworkSynchronization
var disposable: Disposable? = null
@Before
fun setUp() {
SpecificTestConfigurationsFactory.createServerTypeConfiguration()
.invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
hiltRule.inject()
disposable = networkSynchronization.registerNetworkingSynchronization()
}
@After
fun tearDown() {
activityScenario?.moveToState(Lifecycle.State.DESTROYED)
disposable?.dispose()
}
@Test
fun GIVEN_loggedInState_WHEN_opened_THEN_MainActivity_is_started() {
setupLoggedInState.setupLogin(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
activityScenario = ActivityScenario.launch(HiltSplashActivity::class.java)
mainDispatcherTestRule.advanceTimeBy(500)
splashRobot.assertHomeIsStarted()
.assertAuthIsNotStarted()
setupLoggedInState.setupLogout()
}
@Test
fun GIVEN_loggedOffState_WHEN_opened_THEN_AuthActivity_is_started() {
setupLoggedInState.setupLogout()
activityScenario = ActivityScenario.launch(HiltSplashActivity::class.java)
mainDispatcherTestRule.advanceTimeBy(500)
splashRobot.assertAuthIsStarted()
.assertHomeIsNotStarted()
}
}

View file

@ -0,0 +1,34 @@
package org.fnives.test.showcase.testutils
import androidx.test.core.app.ApplicationProvider
import org.fnives.test.showcase.TestShowcaseApplication
import org.fnives.test.showcase.di.BaseUrlProvider
import org.fnives.test.showcase.di.createAppModules
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.GlobalContext
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
class ReloadKoinModulesIfNecessaryTestRule : TestRule {
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
override fun evaluate() {
if (GlobalContext.getOrNull() == null) {
val application =
ApplicationProvider.getApplicationContext<TestShowcaseApplication>()
startKoin {
androidContext(application)
modules(createAppModules(BaseUrlProvider.get()))
}
}
try {
base.evaluate()
} finally {
stopKoin()
}
}
}
}

View file

@ -0,0 +1,31 @@
package org.fnives.test.showcase.testutils.statesetup
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.core.login.LoginUseCase
import org.fnives.test.showcase.core.login.LogoutUseCase
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.koin.test.KoinTest
import org.koin.test.get
object SetupLoggedInState : KoinTest {
private val logoutUseCase get() = get<LogoutUseCase>()
private val loginUseCase get() = get<LoginUseCase>()
private val isUserLoggedInUseCase get() = get<IsUserLoggedInUseCase>()
fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
runBlocking {
loginUseCase.invoke(LoginCredentials("a", "b"))
}
}
fun isLoggedIn() = isUserLoggedInUseCase.invoke()
fun setupLogout() {
runBlocking { logoutUseCase.invoke() }
}
}

View file

@ -0,0 +1,14 @@
package org.fnives.test.showcase.ui
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.home.MainActivity
import org.fnives.test.showcase.ui.splash.SplashActivity
object ActivityClassHolder {
fun authActivity() = AuthActivity::class
fun mainActivity() = MainActivity::class
fun splashActivity() = SplashActivity::class
}

View file

@ -1,6 +1,7 @@
package org.fnives.test.showcase.di
import android.content.Context
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.testutils.TestMainDispatcher
import org.fnives.test.showcase.ui.auth.AuthViewModel
import org.fnives.test.showcase.ui.home.MainViewModel
@ -9,8 +10,11 @@ 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
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.test.KoinTest
import org.koin.test.check.checkModules
import org.koin.test.inject
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
@ -38,20 +42,20 @@ class DITest : KoinTest {
fun verifyStaticModules() {
val mockContext = mock<Context>()
whenever(mockContext.getSharedPreferences(anyOrNull(), anyOrNull())).doReturn(mock())
// checkModules {
// androidContext(mockContext)
// modules(createAppModules(BaseUrl("https://a.com/")))
// }
checkModules {
androidContext(mockContext)
modules(createAppModules(BaseUrl("https://a.com/")))
}
}
@Test
fun verifyViewModelModules() {
val mockContext = mock<Context>()
whenever(mockContext.getSharedPreferences(anyOrNull(), anyOrNull())).doReturn(mock())
// startKoin {
// androidContext(mockContext)
// modules(createAppModules(BaseUrl("https://a.com/")))
// }
startKoin {
androidContext(mockContext)
modules(createAppModules(BaseUrl("https://a.com/")))
}
authViewModel
mainViewModel
splashViewModel

View file

@ -45,7 +45,7 @@ task unitTests(dependsOn: ["app:testKoinDebugUnitTest", "app:testHiltDebugUnitTe
description = 'Run all unit tests'
}
task androidTests(dependsOn: "app:connectedAndroidTest"){
task androidTests(dependsOn: ["app:connectedKoinDebugAndroidTest", "app:connectedHiltDebugAndroidTest"]){
group = 'Tests'
description = 'Run all Android tests'
}

View file

@ -6,11 +6,12 @@ import org.fnives.test.showcase.core.di.hilt.CoreModule
import org.fnives.test.showcase.core.di.hilt.ReloadLoggedInModuleInjectModuleImpl
import org.fnives.test.showcase.core.session.SessionExpirationListener
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.network.di.hilt.BindsBaseOkHttpClient
import org.fnives.test.showcase.network.di.hilt.HiltNetworkModule
import javax.inject.Singleton
@Singleton
@Component(modules = [CoreModule::class, HiltNetworkModule::class, ReloadLoggedInModuleInjectModuleImpl::class])
@Component(modules = [CoreModule::class, HiltNetworkModule::class, ReloadLoggedInModuleInjectModuleImpl::class, BindsBaseOkHttpClient::class])
internal interface TestCoreComponent {

View file

@ -6,4 +6,8 @@ plugins {
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation "javax.inject:javax.inject:1"
}

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.network.di.hilt
package org.fnives.test.showcase.hilt
import javax.inject.Qualifier

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.network.di.hilt
package org.fnives.test.showcase.hilt
import javax.inject.Qualifier

View file

@ -17,9 +17,12 @@ dependencies {
implementation "com.squareup.moshi:moshi:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
// koin
api "io.insert-koin:koin-core:$koin_version"
// hilt
implementation "com.google.dagger:hilt-core:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
api project(":model")
@ -30,5 +33,6 @@ dependencies {
testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
kaptTest "com.google.dagger:dagger-compiler:$hilt_version"
}

View file

@ -0,0 +1,17 @@
package org.fnives.test.showcase.network.di.hilt
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import org.fnives.test.showcase.hilt.SessionLessQualifier
@InstallIn(SingletonComponent::class)
@Module
abstract class BindsBaseOkHttpClient {
@Binds
@SessionLessQualifier
abstract fun bindsSessionLess(okHttpClient: OkHttpClient) : OkHttpClient
}

View file

@ -5,6 +5,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import org.fnives.test.showcase.hilt.SessionLessQualifier
import org.fnives.test.showcase.hilt.SessionQualifier
import org.fnives.test.showcase.network.auth.LoginRemoteSource
import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
import org.fnives.test.showcase.network.auth.LoginService
@ -31,7 +33,6 @@ object HiltNetworkModule {
@Provides
@Singleton
@SessionLessQualifier
fun provideSessionLessOkHttpClient(enableLogging: Boolean) =
OkHttpClient.Builder()
.addInterceptor(PlatformInterceptor())

View file

@ -6,13 +6,14 @@ import org.fnives.test.showcase.network.auth.hilt.LoginRemoteSourceRefreshAction
import org.fnives.test.showcase.network.auth.hilt.LoginRemoteSourceTest
import org.fnives.test.showcase.network.content.hilt.ContentRemoteSourceImplTest
import org.fnives.test.showcase.network.content.hilt.SessionExpirationTest
import org.fnives.test.showcase.network.di.hilt.BindsBaseOkHttpClient
import org.fnives.test.showcase.network.di.hilt.HiltNetworkModule
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import javax.inject.Singleton
@Singleton
@Component(modules = [HiltNetworkModule::class])
@Component(modules = [HiltNetworkModule::class, BindsBaseOkHttpClient::class])
interface TestNetworkComponent {