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 { hilt {
dimension 'di' dimension 'di'
applicationId "org.fnives.test.showcase.hilt" applicationId "org.fnives.test.showcase.hilt"
testInstrumentationRunner "org.fnives.test.showcase.testutils.configuration.HiltTestRunner"
} }
koin { koin {
dimension 'di' dimension 'di'
@ -47,15 +48,24 @@ android {
androidTest { androidTest {
java.srcDirs += "src/sharedTest/java" java.srcDirs += "src/sharedTest/java"
} }
androidTestHilt {
java.srcDirs += "src/sharedTestHilt/java"
}
androidTestKoin {
java.srcDirs += "src/sharedTestKoin/java"
}
test { test {
java.srcDirs += "src/sharedTest/java" java.srcDirs += "src/sharedTest/java"
java.srcDirs += "src/robolectricTest/java" java.srcDirs += "src/robolectricTest/java"
} }
testHilt { testHilt {
java.srcDirs += "src/sharedTestHilt/java"
java.srcDirs += "src/robolectricTestHilt/java" java.srcDirs += "src/robolectricTestHilt/java"
resources.srcDirs += "src/robolectricTestHilt/resources" resources.srcDirs += "src/robolectricTestHilt/resources"
} }
testKoin { testKoin {
java.srcDirs += "src/sharedTestKoin/java"
java.srcDirs += "src/robolectricTestKoin/java" java.srcDirs += "src/robolectricTestKoin/java"
} }
} }
@ -147,4 +157,5 @@ dependencies {
androidTestRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version" androidTestRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version" androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-compiler:$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 //package org.fnives.test.showcase.testutils.statesetup
//
import kotlinx.coroutines.runBlocking //import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase //import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.core.login.LoginUseCase //import org.fnives.test.showcase.core.login.LoginUseCase
import org.fnives.test.showcase.core.login.LogoutUseCase //import org.fnives.test.showcase.core.login.LogoutUseCase
import org.fnives.test.showcase.model.auth.LoginCredentials //import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup //import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario //import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.koin.test.KoinTest //import org.koin.test.KoinTest
import org.koin.test.get //import org.koin.test.get
//
object SetupLoggedInState : KoinTest { //object SetupLoggedInState : KoinTest {
//
private val logoutUseCase get() = get<LogoutUseCase>() // private val logoutUseCase get() = get<LogoutUseCase>()
private val loginUseCase get() = get<LoginUseCase>() // private val loginUseCase get() = get<LoginUseCase>()
private val isUserLoggedInUseCase get() = get<IsUserLoggedInUseCase>() // private val isUserLoggedInUseCase get() = get<IsUserLoggedInUseCase>()
//
fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) { // fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b")) // mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
runBlocking { // runBlocking {
loginUseCase.invoke(LoginCredentials("a", "b")) // loginUseCase.invoke(LoginCredentials("a", "b"))
} // }
} // }
//
fun isLoggedIn() = isUserLoggedInUseCase.invoke() // fun isLoggedIn() = isUserLoggedInUseCase.invoke()
//
fun setupLogout() { // fun setupLogout() {
runBlocking { logoutUseCase.invoke() } // 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.PullToRefresh
import org.fnives.test.showcase.testutils.viewactions.WithDrawable import org.fnives.test.showcase.testutils.viewactions.WithDrawable
import org.fnives.test.showcase.testutils.viewactions.notIntended 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 import org.hamcrest.Matchers.allOf
class HomeRobot : Robot { class HomeRobot : Robot {
override fun init() { override fun init() {
Intents.init() Intents.init()
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName)) Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null)) .respondWith(Instrumentation.ActivityResult(0, null))
} }
@ -37,11 +37,11 @@ class HomeRobot : Robot {
} }
fun assertNavigatedToAuth() = apply { fun assertNavigatedToAuth() = apply {
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName)) Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
} }
fun assertDidNotNavigateToAuth() = apply { fun assertDidNotNavigateToAuth() = apply {
notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName)) notIntended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
} }
fun clickSignOut() = apply { 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.configuration.TestConfigurationsFactory
import org.fnives.test.showcase.testutils.robot.Robot import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.notIntended 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 import org.hamcrest.core.IsNot.not
class LoginRobot( class LoginRobot(
@ -37,7 +37,7 @@ class LoginRobot(
override fun init() { override fun init() {
Intents.init() Intents.init()
intending(hasComponent(MainActivity::class.java.canonicalName)) intending(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent())) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
} }
@ -91,10 +91,10 @@ class LoginRobot(
} }
fun assertNavigatedToHome() = apply { fun assertNavigatedToHome() = apply {
intended(hasComponent(MainActivity::class.java.canonicalName)) intended(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
} }
fun assertNotNavigatedToHome() = apply { 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 androidx.test.espresso.intent.matcher.IntentMatchers
import org.fnives.test.showcase.testutils.robot.Robot import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.notIntended 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.fnives.test.showcase.ui.home.MainActivity
class SplashRobot : Robot { class SplashRobot : Robot {
override fun init() { override fun init() {
Intents.init() Intents.init()
Intents.intending(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName)) Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null)) .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)) .respondWith(Instrumentation.ActivityResult(0, null))
} }
@ -23,18 +22,18 @@ class SplashRobot : Robot {
} }
fun assertHomeIsStarted() = apply { fun assertHomeIsStarted() = apply {
Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName)) Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
} }
fun assertHomeIsNotStarted() = apply { fun assertHomeIsNotStarted() = apply {
notIntended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName)) notIntended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
} }
fun assertAuthIsStarted() = apply { fun assertAuthIsStarted() = apply {
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName)) Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
} }
fun assertAuthIsNotStarted() = apply { 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 package org.fnives.test.showcase.di
import android.content.Context import android.content.Context
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.testutils.TestMainDispatcher import org.fnives.test.showcase.testutils.TestMainDispatcher
import org.fnives.test.showcase.ui.auth.AuthViewModel import org.fnives.test.showcase.ui.auth.AuthViewModel
import org.fnives.test.showcase.ui.home.MainViewModel 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.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith 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.core.context.stopKoin
import org.koin.test.KoinTest import org.koin.test.KoinTest
import org.koin.test.check.checkModules
import org.koin.test.inject import org.koin.test.inject
import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
@ -38,20 +42,20 @@ class DITest : KoinTest {
fun verifyStaticModules() { fun verifyStaticModules() {
val mockContext = mock<Context>() val mockContext = mock<Context>()
whenever(mockContext.getSharedPreferences(anyOrNull(), anyOrNull())).doReturn(mock()) whenever(mockContext.getSharedPreferences(anyOrNull(), anyOrNull())).doReturn(mock())
// checkModules { checkModules {
// androidContext(mockContext) androidContext(mockContext)
// modules(createAppModules(BaseUrl("https://a.com/"))) modules(createAppModules(BaseUrl("https://a.com/")))
// } }
} }
@Test @Test
fun verifyViewModelModules() { fun verifyViewModelModules() {
val mockContext = mock<Context>() val mockContext = mock<Context>()
whenever(mockContext.getSharedPreferences(anyOrNull(), anyOrNull())).doReturn(mock()) whenever(mockContext.getSharedPreferences(anyOrNull(), anyOrNull())).doReturn(mock())
// startKoin { startKoin {
// androidContext(mockContext) androidContext(mockContext)
// modules(createAppModules(BaseUrl("https://a.com/"))) modules(createAppModules(BaseUrl("https://a.com/")))
// } }
authViewModel authViewModel
mainViewModel mainViewModel
splashViewModel splashViewModel

View file

@ -45,7 +45,7 @@ task unitTests(dependsOn: ["app:testKoinDebugUnitTest", "app:testHiltDebugUnitTe
description = 'Run all unit tests' description = 'Run all unit tests'
} }
task androidTests(dependsOn: "app:connectedAndroidTest"){ task androidTests(dependsOn: ["app:connectedKoinDebugAndroidTest", "app:connectedHiltDebugAndroidTest"]){
group = 'Tests' group = 'Tests'
description = 'Run all Android 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.di.hilt.ReloadLoggedInModuleInjectModuleImpl
import org.fnives.test.showcase.core.session.SessionExpirationListener import org.fnives.test.showcase.core.session.SessionExpirationListener
import org.fnives.test.showcase.core.storage.UserDataLocalStorage 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 org.fnives.test.showcase.network.di.hilt.HiltNetworkModule
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@Component(modules = [CoreModule::class, HiltNetworkModule::class, ReloadLoggedInModuleInjectModuleImpl::class]) @Component(modules = [CoreModule::class, HiltNetworkModule::class, ReloadLoggedInModuleInjectModuleImpl::class, BindsBaseOkHttpClient::class])
internal interface TestCoreComponent { internal interface TestCoreComponent {

View file

@ -7,3 +7,7 @@ java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = 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 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 import javax.inject.Qualifier

View file

@ -17,9 +17,12 @@ dependencies {
implementation "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
// koin
api "io.insert-koin:koin-core:$koin_version" api "io.insert-koin:koin-core:$koin_version"
// hilt
implementation "com.google.dagger:hilt-core:$hilt_version" implementation "com.google.dagger:hilt-core:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
api project(":model") api project(":model")
@ -30,5 +33,6 @@ dependencies {
testImplementation "io.insert-koin:koin-test-junit5:$koin_version" testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version" testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_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" 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.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient 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.LoginRemoteSource
import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
import org.fnives.test.showcase.network.auth.LoginService import org.fnives.test.showcase.network.auth.LoginService
@ -31,7 +33,6 @@ object HiltNetworkModule {
@Provides @Provides
@Singleton @Singleton
@SessionLessQualifier
fun provideSessionLessOkHttpClient(enableLogging: Boolean) = fun provideSessionLessOkHttpClient(enableLogging: Boolean) =
OkHttpClient.Builder() OkHttpClient.Builder()
.addInterceptor(PlatformInterceptor()) .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.auth.hilt.LoginRemoteSourceTest
import org.fnives.test.showcase.network.content.hilt.ContentRemoteSourceImplTest import org.fnives.test.showcase.network.content.hilt.ContentRemoteSourceImplTest
import org.fnives.test.showcase.network.content.hilt.SessionExpirationTest 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.di.hilt.HiltNetworkModule
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@Component(modules = [HiltNetworkModule::class]) @Component(modules = [HiltNetworkModule::class, BindsBaseOkHttpClient::class])
interface TestNetworkComponent { interface TestNetworkComponent {