diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml new file mode 100644 index 0000000..1af5df1 --- /dev/null +++ b/.github/workflows/release-package.yml @@ -0,0 +1,27 @@ +name: Library Package Publishing + +on: + release: + types: + - created + +env: + GITHUB_USERNAME: "fknives" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + publish-library: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '11' + - name: Publish Project + run: ./gradlew publishToGitHub \ No newline at end of file diff --git a/README.md b/README.md index 7063de8..9ccd47a 100644 --- a/README.md +++ b/README.md @@ -170,5 +170,59 @@ Open the [shared tests instruction set](./codekata/sharedtests.instructionset.md In this section we will see how can we share Robolectric test source with AndroidTests to run our same tests on actual device. We will also see how to write AndroidTest End to End Tests. + +## Util classes + +Additional modules have been added prefixed with test-util. + +These contain all the reusable Test Util classes used in the showcase. + +The Testing setup is extracted into a separate gradle script, which with some modifications, you should be able to easily add to your own project. + +To use the TestUtil classes, you will need to add the GitHub Repository as a source for dependencies: + +```groovy +// top level build.gradle +allprojects { + repositories { + // ... + maven { + url "https://maven.pkg.github.com/fknives/AndroidTest-ShowCase" + credentials { + username = project.findProperty("GITHUB_USERNAME") ?: System.getenv("GITHUB_USERNAME") + password = project.findProperty("GITHUB_TOKEN") ?: System.getenv("GITHUB_TOKEN") + } + // https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token + } + } +} +// OR +// top level build.gradle.kts +allprojects { + repositories { + // ... + maven { + url = uri("https://maven.pkg.github.com/fknives/AndroidTest-ShowCase") + credentials { + username = extra.properties["GITHUB_USERNAME"] as String? ?: System.getenv("GITHUB_USERNAME") + password = extra.properties["GITHUB_TOKEN"] as String? ?: System.getenv("GITHUB_TOKEN") + } + // https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token + } + } +} +``` + +*Latest version:*![Latest release](https://img.shields.io/github/v/release/fknives/AndroidTest-ShowCase) + +and then you can use the following dependencies: +```groovy +testImplementation "org.fnives.android.testutil:android-unit-junit5:" // test-util-junit5-android +testImplementation "org.fnives.android.testutil:shared-robolectric:" // test-util-shared-robolectric +testImplementation "org.fnives.android.testutil:android:" // test-util-android +androidTestImplementation "org.fnives.android.testutil:android:" // test-util-android +androidTestImplementation "org.fnives.android.testutil:shared-android:" // test-util-shared-android +``` + ## License [License file](./LICENSE) diff --git a/app/build.gradle b/app/build.gradle index b6beea9..d5da099 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,55 +92,25 @@ dependencies { implementation "io.insert-koin:koin-android:$koin_version" implementation "io.insert-koin:koin-androidx-compose:$koin_version" - implementation "androidx.room:room-runtime:$androidx_room_version" - kapt "androidx.room:room-compiler:$androidx_room_version" - implementation "androidx.room:room-ktx:$androidx_room_version" + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-ktx:$room_version" implementation "io.coil-kt:coil:$coil_version" implementation "io.coil-kt:coil-compose:$coil_version" implementation project(":core") - testImplementation "androidx.room:room-testing:$androidx_room_version" - testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" - testImplementation "org.junit.jupiter:junit-jupiter-params:$testing_junit5_version" - testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" - testImplementation "com.jraska.livedata:testing-ktx:$testing_livedata_version" - testImplementation "io.insert-koin:koin-test-junit5:$koin_version" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" + applyAppTestDependenciesTo(this) + applyComposeTestDependenciesTo(this) - // robolectric specific - testImplementation "junit:junit:$testing_junit4_version" - testImplementation "org.robolectric:robolectric:$testing_robolectric_version" - testImplementation "androidx.test:core:$testing_androidx_code_version" - testImplementation "androidx.test:runner:$testing_androidx_code_version" - testImplementation "androidx.test.ext:junit:$testing_androidx_junit_version" - testImplementation "androidx.test.espresso:espresso-core:$testing_espresso_version" - testImplementation "androidx.test.espresso:espresso-intents:$testing_espresso_version" - testImplementation "androidx.test.espresso:espresso-contrib:$testing_espresso_version" - testImplementation project(':mockserver') - testImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version" - testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version" - - androidTestImplementation "androidx.room:room-testing:$androidx_room_version" - androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" - androidTestImplementation "io.insert-koin:koin-test-junit4:$koin_version" - androidTestImplementation "junit:junit:$testing_junit4_version" - androidTestImplementation "androidx.test:core:$testing_androidx_code_version" - androidTestImplementation "androidx.test:runner:$testing_androidx_code_version" - androidTestImplementation "androidx.test.ext:junit:$testing_androidx_junit_version" - androidTestImplementation "androidx.test.espresso:espresso-core:$testing_espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-intents:$testing_espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-contrib:$testing_espresso_version" - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$androidx_compose_version" - testImplementation "androidx.compose.ui:ui-test-junit4:$androidx_compose_version" - debugImplementation "androidx.compose.ui:ui-test-manifest:$androidx_compose_version" androidTestImplementation project(':mockserver') - androidTestImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version" - androidTestRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version" - implementation "io.reactivex.rxjava3:rxjava:3.1.4" + testImplementation project(':test-util-junit5-android') + testImplementation project(':test-util-shared-robolectric') + testImplementation project(':test-util-android') + androidTestImplementation project(':test-util-android') + androidTestImplementation project(':test-util-shared-android') testImplementation testFixtures(project(':core')) androidTestImplementation testFixtures(project(':core')) diff --git a/app/src/androidTest/java/org/fnives/test/showcase/endtoend/LoginLogoutEndToEndTest.kt b/app/src/androidTest/java/org/fnives/test/showcase/endtoend/LoginLogoutEndToEndTest.kt index 911f845..9651ea5 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/endtoend/LoginLogoutEndToEndTest.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/endtoend/LoginLogoutEndToEndTest.kt @@ -11,12 +11,12 @@ import androidx.test.rule.ActivityTestRule import androidx.test.runner.AndroidJUnit4 import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.fnives.test.showcase.R +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.CompositeDisposable +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.Disposable +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.IdlingResourceDisposable +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.OkHttp3IdlingResource +import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper -import org.fnives.test.showcase.testutils.idling.CompositeDisposable -import org.fnives.test.showcase.testutils.idling.Disposable -import org.fnives.test.showcase.testutils.idling.IdlingResourceDisposable -import org.fnives.test.showcase.testutils.idling.OkHttp3IdlingResource -import org.fnives.test.showcase.testutils.idling.loopMainThreadFor import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization import org.fnives.test.showcase.ui.splash.SplashActivity import org.hamcrest.Description diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidMigrationTestRuleFactory.kt b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidMigrationTestRuleFactory.kt deleted file mode 100644 index b4c5395..0000000 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidMigrationTestRuleFactory.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -import android.app.Instrumentation -import androidx.room.RoomDatabase -import androidx.room.migration.AutoMigrationSpec -import androidx.sqlite.db.SupportSQLiteOpenHelper - -object AndroidMigrationTestRuleFactory : SharedMigrationTestRuleFactory { - override fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - databaseClass: Class - ): SharedMigrationTestRule = - AndroidMigrationTestRule( - instrumentation, - databaseClass - ) - - override fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - databaseClass: Class, - specs: List - ): SharedMigrationTestRule = - AndroidMigrationTestRule( - instrumentation, - databaseClass, - specs - ) - - override fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - databaseClass: Class, - specs: List, - openFactory: SupportSQLiteOpenHelper.Factory - ): SharedMigrationTestRule = - AndroidMigrationTestRule( - instrumentation, - databaseClass, - specs, - openFactory, - ) -} diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt deleted file mode 100644 index 974ff95..0000000 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -object SpecificTestConfigurationsFactory : TestConfigurationsFactory { - - override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory = - AndroidMigrationTestRuleFactory -} diff --git a/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt b/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt index 4ee0571..a402297 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/ui/AuthComposeInstrumentedTest.kt @@ -4,13 +4,13 @@ import androidx.compose.ui.test.junit4.StateRestorationTester import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceIdling import org.fnives.test.showcase.compose.screen.AppNavigation import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule -import org.fnives.test.showcase.testutils.idling.DispatcherTestRule -import org.fnives.test.showcase.testutils.idling.anyResourceIdling +import org.fnives.test.showcase.testutils.idling.DatabaseDispatcherTestRule import org.junit.Before import org.junit.Rule import org.junit.Test @@ -27,7 +27,7 @@ class AuthComposeInstrumentedTest : KoinTest { private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule() private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup - private val dispatcherTestRule = DispatcherTestRule() + private val dispatcherTestRule = DatabaseDispatcherTestRule() private lateinit var robot: ComposeLoginRobot private lateinit var navigationRobot: ComposeNavigationRobot diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricMigrationTestHelperFactory.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricMigrationTestHelperFactory.kt deleted file mode 100644 index 887a7b9..0000000 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricMigrationTestHelperFactory.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -import android.app.Instrumentation -import androidx.room.RoomDatabase -import androidx.room.migration.AutoMigrationSpec -import androidx.sqlite.db.SupportSQLiteOpenHelper - -object RobolectricMigrationTestHelperFactory : SharedMigrationTestRuleFactory { - override fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - databaseClass: Class - ): SharedMigrationTestRule = - RobolectricMigrationTestHelper( - instrumentation, - databaseClass - ) - - override fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - databaseClass: Class, - specs: List - ): SharedMigrationTestRule = - RobolectricMigrationTestHelper( - instrumentation, - databaseClass, - specs - ) - - override fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - databaseClass: Class, - specs: List, - openFactory: SupportSQLiteOpenHelper.Factory - ): SharedMigrationTestRule = - RobolectricMigrationTestHelper( - instrumentation, - databaseClass, - specs, - openFactory - ) -} diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt deleted file mode 100644 index e9c281d..0000000 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -object SpecificTestConfigurationsFactory : TestConfigurationsFactory { - - override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory = - RobolectricMigrationTestHelperFactory -} diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricAuthActivityInstrumentedTest.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricAuthActivityInstrumentedTest.kt index 01e6399..c665d5a 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricAuthActivityInstrumentedTest.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricAuthActivityInstrumentedTest.kt @@ -11,15 +11,15 @@ import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.fnives.test.showcase.R +import org.fnives.test.showcase.android.testutil.activity.safeClose +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.CompositeDisposable +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.Disposable +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.IdlingResourceDisposable +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.OkHttp3IdlingResource import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper -import org.fnives.test.showcase.testutils.idling.CompositeDisposable -import org.fnives.test.showcase.testutils.idling.Disposable -import org.fnives.test.showcase.testutils.idling.IdlingResourceDisposable -import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule.Companion.advanceUntilIdleWithIdlingResources -import org.fnives.test.showcase.testutils.idling.OkHttp3IdlingResource -import org.fnives.test.showcase.testutils.safeClose +import org.fnives.test.showcase.testutils.idling.DatabaseDispatcherTestRule.Companion.advanceUntilIdleWithIdlingResources import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization import org.fnives.test.showcase.ui.auth.AuthActivity import org.junit.After diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricLoginRobot.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricLoginRobot.kt index 8ed9cf6..8a912e6 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricLoginRobot.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricLoginRobot.kt @@ -10,14 +10,13 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import org.fnives.test.showcase.R -import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationHelper -import org.fnives.test.showcase.testutils.viewactions.notIntended +import org.fnives.test.showcase.android.testutil.intent.notIntended +import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsNotShown +import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsShownWithText import org.fnives.test.showcase.ui.home.MainActivity import org.hamcrest.core.IsNot.not -class RobolectricLoginRobot( - private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationHelper() -) { +class RobolectricLoginRobot { fun setUsername(username: String): RobolectricLoginRobot = apply { onView(withId(R.id.user_edit_text)) @@ -55,11 +54,11 @@ class RobolectricLoginRobot( } fun assertErrorIsShown(@StringRes stringResID: Int) = apply { - snackbarVerificationHelper.assertIsShownWithText(stringResID) + assertSnackBarIsShownWithText(stringResID) } fun assertErrorIsNotShown() = apply { - snackbarVerificationHelper.assertIsNotShown() + assertSnackBarIsNotShown() } fun assertNavigatedToHome() = apply { diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatestInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatestInstrumentedTest.kt index d615a49..a2527aa 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatestInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatestInstrumentedTest.kt @@ -1,16 +1,14 @@ package org.fnives.test.showcase.storage.migration import androidx.room.Room -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import org.fnives.test.showcase.android.testutil.SharedMigrationTestRule import org.fnives.test.showcase.storage.LocalDatabase import org.fnives.test.showcase.storage.favourite.FavouriteEntity import org.fnives.test.showcase.storage.migation.Migration1To2 -import org.fnives.test.showcase.testutils.configuration.SharedMigrationTestRule -import org.fnives.test.showcase.testutils.configuration.createSharedMigrationTestRule import org.junit.After import org.junit.Assert import org.junit.Rule @@ -28,11 +26,7 @@ import java.io.IOException class MigrationToLatestInstrumentedTest { @get:Rule - val helper: SharedMigrationTestRule = createSharedMigrationTestRule( - InstrumentationRegistry.getInstrumentation(), - emptyList(), - FrameworkSQLiteOpenHelperFactory() - ) + val helper = SharedMigrationTestRule(instrumentation = InstrumentationRegistry.getInstrumentation()) private fun getMigratedRoomDatabase(): LocalDatabase { val database: LocalDatabase = Room.databaseBuilder( diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SharedMigrationTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SharedMigrationTestRule.kt deleted file mode 100644 index f521053..0000000 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SharedMigrationTestRule.kt +++ /dev/null @@ -1,60 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -import android.app.Instrumentation -import androidx.room.RoomDatabase -import androidx.room.migration.AutoMigrationSpec -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import org.junit.rules.TestRule -import java.io.IOException - -interface SharedMigrationTestRule : TestRule { - - @Throws(IOException::class) - fun createDatabase(name: String, version: Int): SupportSQLiteDatabase - - @Throws(IOException::class) - fun runMigrationsAndValidate( - name: String, - version: Int, - validateDroppedTables: Boolean, - vararg migrations: Migration - ): SupportSQLiteDatabase - - fun closeWhenFinished(db: SupportSQLiteDatabase) - fun closeWhenFinished(db: RoomDatabase) -} - -inline fun createSharedMigrationTestRule( - instrumentation: Instrumentation -): SharedMigrationTestRule = - SpecificTestConfigurationsFactory.createSharedMigrationTestRuleFactory() - .createSharedMigrationTestRule( - instrumentation, - DB::class.java - ) - -inline fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - specs: List -): SharedMigrationTestRule = - SpecificTestConfigurationsFactory.createSharedMigrationTestRuleFactory() - .createSharedMigrationTestRule( - instrumentation, - DB::class.java, - specs - ) - -inline fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - specs: List, - openFactory: SupportSQLiteOpenHelper.Factory -): SharedMigrationTestRule = - SpecificTestConfigurationsFactory.createSharedMigrationTestRuleFactory() - .createSharedMigrationTestRule( - instrumentation, - DB::class.java, - specs, - openFactory - ) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SharedMigrationTestRuleFactory.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SharedMigrationTestRuleFactory.kt deleted file mode 100644 index c99713e..0000000 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SharedMigrationTestRuleFactory.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -import android.app.Instrumentation -import androidx.room.RoomDatabase -import androidx.room.migration.AutoMigrationSpec -import androidx.sqlite.db.SupportSQLiteOpenHelper - -interface SharedMigrationTestRuleFactory { - - fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - databaseClass: Class, - ): SharedMigrationTestRule - - fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - databaseClass: Class, - specs: List - ): SharedMigrationTestRule - - fun createSharedMigrationTestRule( - instrumentation: Instrumentation, - databaseClass: Class, - specs: List, - openFactory: SupportSQLiteOpenHelper.Factory - ): SharedMigrationTestRule -} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt deleted file mode 100644 index 18336c9..0000000 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -/** - * Defines the platform specific configurations for Robolectric and AndroidTest. - * - * Each should have an object [SpecificTestConfigurationsFactory] implementing this interface so the SharedTests are - * configured properly. - */ -interface TestConfigurationsFactory { - - fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory -} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/doBlockinglyOnMainThread.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/doBlockinglyOnMainThread.kt deleted file mode 100644 index f8a8e91..0000000 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/doBlockinglyOnMainThread.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.fnives.test.showcase.testutils - -import android.os.Handler -import android.os.Looper -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.runBlocking - -fun runOnUIAwaitOnCurrent(action: () -> Unit) { - if (Looper.myLooper() === Looper.getMainLooper()) { - action() - } else { - val deferred = CompletableDeferred() - Handler(Looper.getMainLooper()).post { - action() - deferred.complete(Unit) - } - runBlocking { deferred.await() } - } -} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DispatcherTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DatabaseDispatcherTestRule.kt similarity index 84% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DispatcherTestRule.kt rename to app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DatabaseDispatcherTestRule.kt index 8d2f0bc..4708d8b 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DispatcherTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DatabaseDispatcherTestRule.kt @@ -3,14 +3,16 @@ package org.fnives.test.showcase.testutils.idling import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher -import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceIdling +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.awaitIdlingResources +import org.fnives.test.showcase.android.testutil.synchronization.runOnUIAwaitOnCurrent import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @OptIn(ExperimentalCoroutinesApi::class) -class DispatcherTestRule : TestRule { +class DatabaseDispatcherTestRule : TestRule { private lateinit var testDispatcher: TestDispatcher diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/Disposable.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/Disposable.kt deleted file mode 100644 index 56a3e04..0000000 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/Disposable.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.fnives.test.showcase.testutils.idling - -interface Disposable { - val isDisposed: Boolean - fun dispose() -} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt index de21704..5c1319d 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt @@ -1,58 +1,14 @@ package org.fnives.test.showcase.testutils.idling -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization -import org.junit.rules.TestRule -import org.junit.runner.Description -import org.junit.runners.model.Statement +import org.fnives.test.showcase.android.testutil.synchronization.MainDispatcherTestRule as LibMainDispatcherTestRule @OptIn(ExperimentalCoroutinesApi::class) -class MainDispatcherTestRule : TestRule { +class MainDispatcherTestRule(useStandard: Boolean = true) : LibMainDispatcherTestRule(useStandard) { - private lateinit var testDispatcher: TestDispatcher - - override fun apply(base: Statement, description: Description): Statement = - object : Statement() { - @Throws(Throwable::class) - override fun evaluate() { - val dispatcher = StandardTestDispatcher() - Dispatchers.setMain(dispatcher) - testDispatcher = dispatcher - TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher) - try { - base.evaluate() - } finally { - Dispatchers.resetMain() - } - } - } - - fun advanceUntilIdleWithIdlingResources() = runOnUIAwaitOnCurrent { - testDispatcher.advanceUntilIdleWithIdlingResources() - } - - fun advanceUntilIdle() = runOnUIAwaitOnCurrent { - testDispatcher.scheduler.advanceUntilIdle() - } - - fun advanceTimeBy(delayInMillis: Long) = runOnUIAwaitOnCurrent { - testDispatcher.scheduler.advanceTimeBy(delayInMillis) - } - - companion object { - fun TestDispatcher.advanceUntilIdleWithIdlingResources() { - scheduler.advanceUntilIdle() // advance until a request is sent - while (anyResourceIdling()) { // check if any request is in progress - awaitIdlingResources() // complete all requests and other idling resources - scheduler.advanceUntilIdle() // run coroutines after request is finished - } - scheduler.advanceUntilIdle() - } + override fun onTestDispatcherInitialized(testDispatcher: TestDispatcher) { + TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatcher) } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt index d2cf705..ee3cff5 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt @@ -3,6 +3,10 @@ package org.fnives.test.showcase.testutils.idling import androidx.annotation.CheckResult import androidx.test.espresso.IdlingResource import okhttp3.OkHttpClient +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.CompositeDisposable +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.Disposable +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.IdlingResourceDisposable +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.OkHttp3IdlingResource import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper import org.junit.rules.TestRule import org.junit.runner.Description diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt index 7ffc9dd..411f6ea 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt @@ -4,10 +4,10 @@ import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.runner.intent.IntentStubberRegistry +import org.fnives.test.showcase.android.testutil.activity.safeClose import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule -import org.fnives.test.showcase.testutils.safeClose import org.fnives.test.showcase.ui.auth.AuthActivity import org.fnives.test.showcase.ui.home.HomeRobot import org.fnives.test.showcase.ui.home.MainActivity @@ -19,7 +19,7 @@ object SetupAuthenticationState : KoinTest { fun setupLogin( mainDispatcherTestRule: MainDispatcherTestRule, mockServerScenarioSetup: MockServerScenarioSetup, - resetIntents: Boolean = true + resetIntents: Boolean = true, ) { resetIntentsIfNeeded(resetIntents) { mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b")) @@ -40,7 +40,7 @@ object SetupAuthenticationState : KoinTest { fun setupLogout( mainDispatcherTestRule: MainDispatcherTestRule, - resetIntents: Boolean = true + resetIntents: Boolean = true, ) { resetIntentsIfNeeded(resetIntents) { val activityScenario = ActivityScenario.launch(MainActivity::class.java) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt index fe95319..7b154a6 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt @@ -19,11 +19,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withText import org.fnives.test.showcase.R +import org.fnives.test.showcase.android.testutil.intent.notIntended +import org.fnives.test.showcase.android.testutil.viewaction.imageview.WithDrawable +import org.fnives.test.showcase.android.testutil.viewaction.swiperefresh.PullToRefresh import org.fnives.test.showcase.model.content.Content import org.fnives.test.showcase.model.content.FavouriteContent -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.hamcrest.Matchers.allOf diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt index bc412e8..1517396 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt @@ -3,6 +3,8 @@ package org.fnives.test.showcase.ui.home import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.fnives.test.showcase.android.testutil.activity.safeClose +import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor 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 @@ -10,9 +12,6 @@ import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshToken import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule import org.fnives.test.showcase.testutils.idling.AsyncDiffUtilInstantTestRule import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule -import org.fnives.test.showcase.testutils.idling.loopMainThreadFor -import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources -import org.fnives.test.showcase.testutils.safeClose import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin import org.junit.After import org.junit.Before @@ -175,9 +174,6 @@ class MainActivityInstrumentedTest : KoinTest { robot.swipeRefresh() mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - loopMainThreadUntilIdleWithIdlingResources() - mainDispatcherTestRule.advanceTimeBy(1000L) - loopMainThreadFor(1000) robot .assertContainsError() diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt index a54e6f1..7b31b8c 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt @@ -4,10 +4,10 @@ import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R +import org.fnives.test.showcase.android.testutil.activity.safeClose import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule -import org.fnives.test.showcase.testutils.safeClose import org.fnives.test.showcase.ui.auth.AuthActivity import org.junit.After import org.junit.Before diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt index 0419eec..6ec7cd5 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt @@ -14,15 +14,14 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import org.fnives.test.showcase.R -import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationHelper -import org.fnives.test.showcase.testutils.viewactions.ReplaceProgressBarDrawableToStatic -import org.fnives.test.showcase.testutils.viewactions.notIntended +import org.fnives.test.showcase.android.testutil.intent.notIntended +import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsNotShown +import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsShownWithText +import org.fnives.test.showcase.android.testutil.viewaction.progressbar.ReplaceProgressBarDrawableToStatic import org.fnives.test.showcase.ui.home.MainActivity import org.hamcrest.core.IsNot.not -class LoginRobot( - private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationHelper() -) { +class LoginRobot { fun setupIntentResults() { Intents.intending(hasComponent(MainActivity::class.java.canonicalName)) @@ -68,7 +67,7 @@ class LoginRobot( } fun assertErrorIsShown(@StringRes stringResID: Int) = apply { - snackbarVerificationHelper.assertIsShownWithText(stringResID) + assertSnackBarIsShownWithText(stringResID) } fun assertLoadingBeforeRequests() = apply { @@ -82,7 +81,7 @@ class LoginRobot( } fun assertErrorIsNotShown() = apply { - snackbarVerificationHelper.assertIsNotShown() + assertSnackBarIsNotShown() } fun assertNavigatedToHome() = apply { diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/codekata/CodeKataSharedRobotTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/codekata/CodeKataSharedRobotTest.kt index fb87018..2123cab 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/codekata/CodeKataSharedRobotTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/codekata/CodeKataSharedRobotTest.kt @@ -8,14 +8,13 @@ import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.matcher.ViewMatchers import org.fnives.test.showcase.R -import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationHelper -import org.fnives.test.showcase.testutils.viewactions.notIntended +import org.fnives.test.showcase.android.testutil.intent.notIntended +import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsNotShown +import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsShownWithText import org.fnives.test.showcase.ui.home.MainActivity import org.hamcrest.core.IsNot -class CodeKataSharedRobotTest( - private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationHelper() -) { +class CodeKataSharedRobotTest { fun setUsername(username: String): CodeKataSharedRobotTest = apply { Espresso.onView(ViewMatchers.withId(R.id.user_edit_text)) @@ -53,11 +52,11 @@ class CodeKataSharedRobotTest( } fun assertErrorIsShown(@StringRes stringResID: Int): CodeKataSharedRobotTest = apply { - snackbarVerificationHelper.assertIsShownWithText(stringResID) + assertSnackBarIsShownWithText(stringResID) } fun assertErrorIsNotShown(): CodeKataSharedRobotTest = apply { - snackbarVerificationHelper.assertIsNotShown() + assertSnackBarIsNotShown() } fun assertNavigatedToHome(): CodeKataSharedRobotTest = apply { diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt index cb12061..94e34d8 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt @@ -4,9 +4,9 @@ import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.fnives.test.showcase.android.testutil.activity.safeClose import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule -import org.fnives.test.showcase.testutils.safeClose import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogout import org.junit.After diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt index 0e941b6..c598d77 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt @@ -5,7 +5,7 @@ import android.app.Instrumentation import android.content.Intent import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers -import org.fnives.test.showcase.testutils.viewactions.notIntended +import org.fnives.test.showcase.android.testutil.intent.notIntended import org.fnives.test.showcase.ui.auth.AuthActivity import org.fnives.test.showcase.ui.home.MainActivity diff --git a/app/src/test/java/org/fnives/test/showcase/di/DITest.kt b/app/src/test/java/org/fnives/test/showcase/di/DITest.kt index cf595ba..087272f 100644 --- a/app/src/test/java/org/fnives/test/showcase/di/DITest.kt +++ b/app/src/test/java/org/fnives/test/showcase/di/DITest.kt @@ -1,8 +1,8 @@ package org.fnives.test.showcase.di import android.content.Context +import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher 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 import org.fnives.test.showcase.ui.splash.SplashViewModel @@ -20,7 +20,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -@ExtendWith(TestMainDispatcher::class) +@ExtendWith(StandardTestMainDispatcher::class) class DITest : KoinTest { private val authViewModel by inject() diff --git a/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt b/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt deleted file mode 100644 index 77beff4..0000000 --- a/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.fnives.test.showcase.testutils - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.fnives.test.showcase.testutils.TestMainDispatcher.Companion.testDispatcher -import org.junit.jupiter.api.extension.AfterEachCallback -import org.junit.jupiter.api.extension.BeforeEachCallback -import org.junit.jupiter.api.extension.ExtensionContext - -/** - * Custom Junit5 Extension which replaces the main dispatcher with a [TestDispatcher] - * - * One can access the test dispatcher via [testDispatcher] static getter. - */ -@OptIn(ExperimentalCoroutinesApi::class) -class TestMainDispatcher : BeforeEachCallback, AfterEachCallback { - - override fun beforeEach(context: ExtensionContext?) { - val testDispatcher = StandardTestDispatcher() - privateTestDispatcher = testDispatcher - Dispatchers.setMain(testDispatcher) - } - - override fun afterEach(context: ExtensionContext?) { - Dispatchers.resetMain() - privateTestDispatcher = null - } - - companion object { - private var privateTestDispatcher: TestDispatcher? = null - val testDispatcher: TestDispatcher - get() = privateTestDispatcher - ?: throw IllegalStateException("TestMainDispatcher is in afterEach State") - } -} diff --git a/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt b/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt index 8d9a4af..d1008bf 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/auth/AuthViewModelTest.kt @@ -3,12 +3,12 @@ package org.fnives.test.showcase.ui.auth import com.jraska.livedata.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking +import org.fnives.test.showcase.android.testutil.InstantExecutorExtension +import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher import org.fnives.test.showcase.core.login.LoginUseCase 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.fnives.test.showcase.testutils.InstantExecutorExtension -import org.fnives.test.showcase.testutils.TestMainDispatcher import org.fnives.test.showcase.ui.shared.Event import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName @@ -27,13 +27,13 @@ import org.mockito.kotlin.whenever import java.util.stream.Stream @Suppress("TestFunctionName") -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@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() = TestMainDispatcher.testDispatcher.scheduler + private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { @@ -169,7 +169,7 @@ internal class AuthViewModelTest { @ParameterizedTest(name = "GIVEN answer success loginStatus {0} WHEN login called THEN error {1} is shown") fun invalidStatusResultsInErrorState( loginStatus: LoginStatus, - errorType: AuthViewModel.ErrorType + errorType: AuthViewModel.ErrorType, ) { runBlocking { whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Success(loginStatus)) diff --git a/app/src/test/java/org/fnives/test/showcase/ui/auth/CodeKataAuthViewModel.kt b/app/src/test/java/org/fnives/test/showcase/ui/auth/CodeKataAuthViewModel.kt index 7a70ed0..3d55565 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/auth/CodeKataAuthViewModel.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/auth/CodeKataAuthViewModel.kt @@ -1,22 +1,22 @@ package org.fnives.test.showcase.ui.auth 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.core.login.LoginUseCase -import org.fnives.test.showcase.testutils.InstantExecutorExtension -import org.fnives.test.showcase.testutils.TestMainDispatcher 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.mock -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class) @OptIn(ExperimentalCoroutinesApi::class) class CodeKataAuthViewModel { private lateinit var sut: AuthViewModel private lateinit var mockLoginUseCase: LoginUseCase - private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler + private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { diff --git a/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt b/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt index 6ce1a3c..f4d521c 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/home/MainViewModelTest.kt @@ -4,6 +4,8 @@ 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.core.content.AddContentToFavouriteUseCase import org.fnives.test.showcase.core.content.FetchContentUseCase import org.fnives.test.showcase.core.content.GetAllContentUseCase @@ -14,22 +16,20 @@ 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.fnives.test.showcase.testutils.InstantExecutorExtension -import org.fnives.test.showcase.testutils.TestMainDispatcher 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.verifyNoInteractions import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever @Suppress("TestFunctionName") -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class) @OptIn(ExperimentalCoroutinesApi::class) internal class MainViewModelTest { @@ -39,7 +39,7 @@ internal class MainViewModelTest { private lateinit var mockFetchContentUseCase: FetchContentUseCase private lateinit var mockAddContentToFavouriteUseCase: AddContentToFavouriteUseCase private lateinit var mockRemoveContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase - private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler + private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { diff --git a/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt b/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt index bd366e1..adcd7c7 100644 --- a/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt +++ b/app/src/test/java/org/fnives/test/showcase/ui/splash/SplashViewModelTest.kt @@ -2,9 +2,9 @@ package org.fnives.test.showcase.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.core.login.IsUserLoggedInUseCase -import org.fnives.test.showcase.testutils.InstantExecutorExtension -import org.fnives.test.showcase.testutils.TestMainDispatcher import org.fnives.test.showcase.ui.shared.Event import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName @@ -14,13 +14,13 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@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() = TestMainDispatcher.testDispatcher.scheduler + private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler @BeforeEach fun setUp() { diff --git a/build.gradle b/build.gradle index 84e7132..b40d313 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,14 @@ allprojects { repositories { mavenCentral() google() + maven { + url "https://maven.pkg.github.com/fknives/AndroidTest-ShowCase" + credentials { + username = project.findProperty("GITHUB_USERNAME") ?: System.getenv("GITHUB_USERNAME") + password = project.findProperty("GITHUB_TOKEN") ?: System.getenv("GITHUB_TOKEN") + } + // https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token + } } } @@ -33,4 +41,6 @@ apply from: 'gradlescripts/versions.gradle' apply from: 'gradlescripts/detekt.config.gradle' apply from: 'gradlescripts/ktlint.gradle' apply from: 'gradlescripts/lint.gradle' -apply from: 'gradlescripts/testoptions.gradle' \ No newline at end of file +apply from: 'gradlescripts/testoptions.gradle' +apply from: 'gradlescripts/test.tasks.gradle' +apply from: 'gradlescripts/testdependencies.gradle' \ No newline at end of file diff --git a/codekata/viewmodel.instructionset.md b/codekata/viewmodel.instructionset.md index e7c7f3e..b819eea 100644 --- a/codekata/viewmodel.instructionset.md +++ b/codekata/viewmodel.instructionset.md @@ -25,12 +25,12 @@ Our test class is `org.fnives.test.showcase.ui.splash.CodeKataSplashViewModelTes To properly test LiveData we need to make them instant, meaning as soon as the value is set the observers are updated. To Do this we can use a `InstantExecutorExtension`. -Also We need to set MainDispatcher as TestDispatcher, for this we can use the `TestMainDispatcher` Extension. +Also We need to set MainDispatcher as TestDispatcher, for this we can use the `StandardTestMainDispatcher` Extension. To add this to our TestClass we need to do the following: ```kotlin -@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) +@ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class) class CodeKataSplashViewModelTest { ``` @@ -41,7 +41,7 @@ Next let's set up our System Under Test as usual: ```kotlin private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase private lateinit var sut: SplashViewModel -private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler // just a shortcut +private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler // just a shortcut @BeforeEach fun setUp() { @@ -69,7 +69,7 @@ val navigateToTestObserver = sut.navigateTo.test() Since the action takes place in the ViewModel constructor, instead of additional calls, we need to simulate that time has elapsed. -Note: the `TestMainDispatcher` Extension we are using sets `StandardTestDispatcher` as the dispatcher for `Dispatcher.Main`, that's why our test is linear and not shaky. +Note: the `StandardTestMainDispatcher` Extension we are using sets `StandardTestDispatcher` as the dispatcher for `Dispatcher.Main`, that's why our test is linear and not shaky. ```kotlin testScheduler.advanceTimeBy(501) diff --git a/core/build.gradle b/core/build.gradle index 617bb40..ebc50e5 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -20,15 +20,7 @@ dependencies { api project(":model") implementation project(":network") - testImplementation "io.insert-koin:koin-test-junit5:$koin_version" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" - testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version" - testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" - testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version" - testImplementation "app.cash.turbine:turbine:$turbine_version" - - testImplementation "org.junit.jupiter:junit-jupiter-params:$testing_junit5_version" + applyCoreTestDependenciesTo(this) testImplementation project(':mockserver') testFixturesApi testFixtures(project(':network')) diff --git a/gradle.properties b/gradle.properties index dbc9506..c3dcb2c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,5 @@ android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=false # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official +android.disableAutomaticComponentCreation=true diff --git a/gradlescripts/deploy.aar.gradle b/gradlescripts/deploy.aar.gradle new file mode 100644 index 0000000..fcc77df --- /dev/null +++ b/gradlescripts/deploy.aar.gradle @@ -0,0 +1,44 @@ +apply plugin: "maven-publish" + +def testUtilVersion = "1.0.1" +if (!extensions.extraProperties.has("artifactId")) { + throw IllegalStateException("ext.artifactId is not set while applying deploy script") +} +def testUtilGroupId = "org.fnives.android.testutil" +def testUtilArtifactId = extensions.extraProperties.get("artifactId") + +task publishToGitHub(dependsOn: "publishMavenAarPublicationToGitHubPackagesRepository") { + group = "Publishing" +} + +task sourcesJar(type: Jar) { + from android.sourceSets.main.java.srcDirs + classifier "sources" +} + +afterEvaluate { + publishing { + publications { + mavenAar(MavenPublication) { + from components.release + + groupId "$testUtilGroupId" + println("$testUtilArtifactId") + version "$testUtilVersion" + artifactId "$testUtilArtifactId" + artifact sourcesJar + } + } + + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/fknives/AndroidTest-ShowCase") + credentials { + username = System.getenv("GITHUB_USERNAME") + password = System.getenv("GITHUB_TOKEN") + } + } + } + } +} \ No newline at end of file diff --git a/gradlescripts/test.tasks.gradle b/gradlescripts/test.tasks.gradle new file mode 100644 index 0000000..c8f1744 --- /dev/null +++ b/gradlescripts/test.tasks.gradle @@ -0,0 +1,16 @@ +task jvmTests(dependsOn: ["app:testDebugUnitTest", "core:test", "network:test"]) { + group = 'Tests' + description = 'Run all JVM tests' +} + +task robolectricTests(type: Exec) { + group = 'Tests' + description = 'Run all Robolectric tests based on the Instrumented naming convention' + // todo is there a better way? + commandLine 'sh', './gradlew', 'testDebugUnitTest', '--tests', 'org.fnives.test.*InstrumentedTest' +} + +task androidTests(dependsOn: ["app:connectedDebugAndroidTest"]) { + group = 'Tests' + description = 'Run Android tests' +} \ No newline at end of file diff --git a/gradlescripts/testdependencies.gradle b/gradlescripts/testdependencies.gradle new file mode 100644 index 0000000..f4ce4b8 --- /dev/null +++ b/gradlescripts/testdependencies.gradle @@ -0,0 +1,148 @@ +project.ext { + + /* + ------------------USAGE------------------ + add line in root build.gradle: + apply from: 'gradlescripts/testdependencies.gradle' (this file) + then in your modules you can use: + + ------------------NETWORK(Java Module)------------------ + dependencies { + applyNetworkTestDependenciesTo(this) + } + + ------------------CORE(Java Module)------------------ + dependencies { + applyCoreTestDependenciesTo(this) + } + + ------------------APP(Android Module)------------------ + dependencies { + applyAppTestDependenciesTo(this) + applyComposeTestDependenciesTo(this) // if you are using compose + } + + ------------------VERSIONS------------------ + versions try to get the global value, if not found they fall back to some defaults. + You can find them just below + */ + + def propertyOrNull = { key -> + if (extensions.extraProperties.has(key)) { + return extensions.extraProperties.get(key) + } else { + return null + } + } + + // ------------------VERSIONS------------------ + def testing_junit5_version = propertyOrNull('junit5_version') ?: "5.7.0" + def testing_junit4_version = propertyOrNull('junit4_version') ?: "4.13.2" + def testing_robolectric_version = propertyOrNull('robolectric_version') ?: "4.7" + def testing_androidx_code_version = propertyOrNull('androidx_test_version') ?: "1.4.0" + def testing_androidx_junit_version = propertyOrNull('androidx_junit_version') ?: "1.1.3" + def testing_espresso_version = propertyOrNull('espresso_version') ?: "3.4.0" + def testing_androidx_arch_core_version = propertyOrNull('arch_core_version') ?: "2.1.0" + def test_coroutines_version = propertyOrNull('coroutines_version') ?: "1.6.0" + def testing_kotlin_mockito_version = propertyOrNull('mockito_version') ?: "4.0.0" + def testing_koin_version = propertyOrNull('koin_version') ?: "3.1.2" + def testing_json_assert_version = propertyOrNull('json_assert_version') ?: "1.5.0" + def testing_okhttp3 = propertyOrNull('okhttp_version') ?: "4.9.3" + def testing_turbine_version = propertyOrNull('turbine_version') ?: "0.7.0" + def testing_androidx_room_version = propertyOrNull('room_version') ?: "2.4.2" + def testing_livedata_version = propertyOrNull('testing_livedata_version') ?: "1.2.0" + def testing_compose_version = propertyOrNull('androidx_compose_version') ?: "1.1.0" + def testing_hamcrest_version = propertyOrNull('hamcrest_version') ?: "2.2" + + // ------------------PRIVATE------------------ + // JUni4 + Espresso + Room + def androidSpecificTestDependencies = [ + "junit:junit:$testing_junit4_version", + + "androidx.room:room-testing:$testing_androidx_room_version", + + "com.jraska.livedata:testing-ktx:$testing_livedata_version", + + "androidx.test:core:$testing_androidx_code_version", + "androidx.test:runner:$testing_androidx_code_version", + + "androidx.test.ext:junit:$testing_androidx_junit_version", + + "androidx.test.espresso:espresso-core:$testing_espresso_version", + "androidx.test.espresso:espresso-intents:$testing_espresso_version", + "androidx.test.espresso:espresso-contrib:$testing_espresso_version", + "org.hamcrest:hamcrest:$testing_hamcrest_version", + + "androidx.arch.core:core-testing:$testing_androidx_arch_core_version", + ] + + // ------------------PRIVATE------------------ + def applyStandardTestDependenciesTo = { module -> + module.dependencies { + // coroutine testing + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$test_coroutines_version" + + // mockito, mocking library + testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version" + + testImplementation "io.insert-koin:koin-test-junit5:$testing_koin_version" + // junit5 + testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" + testImplementation "org.junit.jupiter:junit-jupiter-params:$testing_junit5_version" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" + } + } + + // ------------------NETWORK------------------ + applyNetworkTestDependenciesTo = { module -> + applyStandardTestDependenciesTo(module) + + module.dependencies { + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" + // JSON Assert + testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version" + + // mockwebserver + https support + testImplementation "com.squareup.okhttp3:mockwebserver:$testing_okhttp3" + testImplementation "com.squareup.okhttp3:okhttp-tls:$testing_okhttp3" + } + } + + // ------------------CORE------------------ + applyCoreTestDependenciesTo = { module -> + applyStandardTestDependenciesTo(module) + + module.dependencies { + // turbine, flow testing + testImplementation "app.cash.turbine:turbine:$testing_turbine_version" + } + } + + // ------------------APP------------------ + applyAppTestDependenciesTo = { module -> + applyStandardTestDependenciesTo(module) + + module.dependencies { + testImplementation "org.robolectric:robolectric:$testing_robolectric_version" + testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version" + + androidSpecificTestDependencies.forEach { dependency -> + testImplementation dependency + androidTestImplementation dependency + } + + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$test_coroutines_version" + androidTestImplementation "io.insert-koin:koin-test-junit5:$testing_koin_version" + androidTestRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version" + } + } + + // ------------------COMPOSE------------------ + applyComposeTestDependenciesTo = { module -> + module.dependencies { + testImplementation "androidx.compose.ui:ui-test-junit4:$testing_compose_version" + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$testing_compose_version" + debugImplementation "androidx.compose.ui:ui-test-manifest:$testing_compose_version" + } + } +} \ No newline at end of file diff --git a/gradlescripts/testoptions.gradle b/gradlescripts/testoptions.gradle index ce6575a..7193f48 100644 --- a/gradlescripts/testoptions.gradle +++ b/gradlescripts/testoptions.gradle @@ -62,21 +62,4 @@ subprojects { module -> } } } -} - -task jvmTests(dependsOn: ["app:testDebugUnitTest", "core:test", "network:test"]) { - group = 'Tests' - description = 'Run all JVM tests' -} - -task robolectricTests(type: Exec) { - group = 'Tests' - description = 'Run all Robolectric tests based on the Instrumented naming convention' - // todo is there a better way? - commandLine 'sh', './gradlew', 'testDebugUnitTest', '--tests', 'org.fnives.test.*InstrumentedTest' -} - -task androidTests(dependsOn: ["app:connectedDebugAndroidTest"]) { - group = 'Tests' - description = 'Run Android tests' } \ No newline at end of file diff --git a/gradlescripts/versions.gradle b/gradlescripts/versions.gradle index 3732bda..ff127d6 100644 --- a/gradlescripts/versions.gradle +++ b/gradlescripts/versions.gradle @@ -1,11 +1,11 @@ project.ext { - androidx_core_version = "1.7.0" + androidx_core_version = "1.8.0" androidx_appcompat_version = "1.4.1" - androidx_material_version = "1.5.0" + androidx_material_version = "1.6.1" androidx_constraintlayout_version = "2.1.3" androidx_livedata_version = "2.4.0" androidx_swiperefreshlayout_version = "1.1.0" - androidx_room_version = "2.4.1" + room_version = "2.4.2" activity_ktx_version = "1.4.0" androidx_navigation = "2.4.0" @@ -14,21 +14,23 @@ project.ext { androidx_compose_constraintlayout_version = "1.0.0" coroutines_version = "1.6.0" - turbine_version = "0.7.0" koin_version = "3.1.2" coil_version = "1.4.0" retrofit_version = "2.9.0" okhttp_version = "4.9.3" moshi_version = "1.13.0" - testing_androidx_code_version = "1.4.0" - testing_androidx_junit_version = "1.1.3" - testing_androidx_arch_core_version = "2.1.0" + // testing versions + turbine_version = "0.7.0" + androidx_test_version = "1.4.0" + androidx_junit_version = "1.1.3" + arch_core_version = "2.1.0" testing_livedata_version = "1.2.0" - testing_kotlin_mockito_version = "4.0.0" - testing_junit5_version = "5.7.0" - testing_json_assert_version = "1.5.0" - testing_junit4_version = "4.13.2" - testing_robolectric_version = "4.7" - testing_espresso_version = "3.4.0" + mockito_version = "4.0.0" + junit5_version = "5.7.0" + json_assert_version = "1.5.0" + junit4_version = "4.13.2" + robolectric_version = "4.7" + espresso_version = "3.4.0" + hamcrest_version = "2.2" } \ No newline at end of file diff --git a/mockserver/build.gradle b/mockserver/build.gradle index 4b481ef..03f8e01 100644 --- a/mockserver/build.gradle +++ b/mockserver/build.gradle @@ -14,7 +14,7 @@ dependencies { api "com.squareup.okhttp3:mockwebserver:$okhttp_version" api "com.squareup.okhttp3:okhttp-tls:$okhttp_version" - implementation "org.skyscreamer:jsonassert:$testing_json_assert_version" - implementation "junit:junit:$testing_junit4_version" + implementation "org.skyscreamer:jsonassert:$json_assert_version" + implementation "junit:junit:$junit4_version" } \ No newline at end of file diff --git a/network/build.gradle b/network/build.gradle index e8cf19c..db2bc59 100644 --- a/network/build.gradle +++ b/network/build.gradle @@ -24,12 +24,9 @@ dependencies { api project(":model") - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" - testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version" - testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" + applyNetworkTestDependenciesTo(this) testFixturesApi project(':mockserver') - testFixturesApi "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" + testFixturesApi "org.junit.jupiter:junit-jupiter-engine:$junit5_version" testFixturesApi "io.insert-koin:koin-test-junit5:$koin_version" } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a7f91d5..6b11768 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,10 @@ +rootProject.name = "TestShowCase" include ':mockserver' include ':model' include ':core' include ':network' include ':app' -rootProject.name = "TestShowCase" \ No newline at end of file +include ':test-util-shared-android' +include ':test-util-shared-robolectric' +include ':test-util-android' +include ':test-util-junit5-android' diff --git a/test-util-android/.gitignore b/test-util-android/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/test-util-android/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/test-util-android/build.gradle b/test-util-android/build.gradle new file mode 100644 index 0000000..c379b2b --- /dev/null +++ b/test-util-android/build.gradle @@ -0,0 +1,56 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 31 + + defaultConfig { + minSdk 21 + targetSdk 31 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + buildConfig = false + } + packagingOptions { + exclude 'META-INF/LGPL2.1' + exclude 'META-INF/AL2.0' + exclude 'META-INF/LICENSE.md' + exclude 'META-INF/LICENSE-notice.md' + } +} + +dependencies { + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" + + implementation "androidx.test:core:$androidx_test_version" + implementation "androidx.test.espresso:espresso-core:$espresso_version" + implementation "androidx.test.espresso:espresso-intents:$espresso_version" + + implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + + implementation "com.google.android.material:material:$androidx_material_version" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version" + implementation "androidx.core:core-ktx:$androidx_core_version" +} + +ext.artifactId = "android" +apply from: "../gradlescripts/deploy.aar.gradle" \ No newline at end of file diff --git a/test-util-android/consumer-rules.pro b/test-util-android/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/test-util-android/proguard-rules.pro b/test-util-android/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/test-util-android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/test-util-android/src/main/AndroidManifest.xml b/test-util-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..80e5dbb --- /dev/null +++ b/test-util-android/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/activity/ActivityScenarioExtensions.kt similarity index 61% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/activity/ActivityScenarioExtensions.kt index 1bf914c..3016b96 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/activity/ActivityScenarioExtensions.kt @@ -1,19 +1,24 @@ -package org.fnives.test.showcase.testutils +package org.fnives.test.showcase.android.testutil.activity import android.app.Activity import androidx.test.core.app.ActivityScenario +/** + * Workaround for issue: https://github.com/android/android-test/issues/676. + * + * Call this instead of ActivityScenario.close(). + */ fun ActivityScenario.safeClose() { workaroundForActivityScenarioCLoseLockingUp() close() } /** - * This should not be needed, we shouldn't use sleep ever. + * This should not be needed, we shouldn't use sleep basically ever. * However, it seems to be and issue described here: https://github.com/android/android-test/issues/676 * * If an activity is finished in code, the ActivityScenario.close() can hang 30 to 45 seconds. - * This sleeps let's the Activity finish it state change and unlocks the ActivityScenario. + * This sleep let's the Activity finish it's state change and unlocks the ActivityScenario. * * As soon as that issue is closed, this should be removed as well. */ diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/notIntended.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/intent/notIntended.kt similarity index 84% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/notIntended.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/intent/notIntended.kt index a88cf48..a9c6123 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/notIntended.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/intent/notIntended.kt @@ -1,10 +1,11 @@ -package org.fnives.test.showcase.testutils.viewactions +package org.fnives.test.showcase.android.testutil.intent import android.content.Intent import androidx.test.espresso.intent.Intents.intended import org.hamcrest.Matcher import org.hamcrest.StringDescription +@Suppress("SwallowedException") fun notIntended(matcher: Matcher) { try { intended(matcher) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/snackbar/SnackbarVerificationHelper.kt similarity index 56% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/snackbar/SnackbarVerificationHelper.kt index cdba01c..6065d14 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/snackbar/SnackbarVerificationHelper.kt @@ -1,5 +1,6 @@ -package org.fnives.test.showcase.testutils.configuration +package org.fnives.test.showcase.android.testutil.snackbar +import android.annotation.SuppressLint import android.view.View import androidx.annotation.StringRes import androidx.test.espresso.Espresso @@ -8,22 +9,25 @@ import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers -import com.google.android.material.R import com.google.android.material.snackbar.Snackbar import org.hamcrest.Matcher import org.hamcrest.Matchers +import com.google.android.material.R as MaterialR -class SnackbarVerificationHelper { +object SnackbarVerificationHelper { - fun assertIsShownWithText(@StringRes stringResID: Int) { - Espresso.onView(ViewMatchers.withId(R.id.snackbar_text)) + @SuppressLint("RestrictedApi") + fun assertSnackBarIsShownWithText(@StringRes stringResID: Int, doDismiss: Boolean = true) { + Espresso.onView(ViewMatchers.withId(MaterialR.id.snackbar_text)) .check(ViewAssertions.matches(ViewMatchers.withText(stringResID))) - Espresso.onView(ViewMatchers.isAssignableFrom(Snackbar.SnackbarLayout::class.java)).perform(ViewActions.swipeRight()) - Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainUntilSnackbarDismissed()) + if (doDismiss) { + Espresso.onView(ViewMatchers.isAssignableFrom(Snackbar.SnackbarLayout::class.java)).perform(ViewActions.swipeRight()) + Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainUntilSnackbarDismissed()) + } } - fun assertIsNotShown() { - Espresso.onView(ViewMatchers.withId(R.id.snackbar_text)).check(ViewAssertions.doesNotExist()) + fun assertSnackBarIsNotShown() { + Espresso.onView(ViewMatchers.withId(MaterialR.id.snackbar_text)).check(ViewAssertions.doesNotExist()) } class LoopMainUntilSnackbarDismissed : ViewAction { diff --git a/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/MainDispatcherTestRule.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/MainDispatcherTestRule.kt new file mode 100644 index 0000000..8e2676d --- /dev/null +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/MainDispatcherTestRule.kt @@ -0,0 +1,64 @@ +package org.fnives.test.showcase.android.testutil.synchronization + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceIdling +import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.awaitIdlingResources +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +@OptIn(ExperimentalCoroutinesApi::class) +open class MainDispatcherTestRule(private val useStandard: Boolean = true) : TestRule { + + private lateinit var testDispatcher: TestDispatcher + + override fun apply(base: Statement, description: Description): Statement = + object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + val dispatcher = if (useStandard) StandardTestDispatcher() else UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) + testDispatcher = dispatcher + onTestDispatcherInitialized(testDispatcher) + try { + base.evaluate() + } finally { + Dispatchers.resetMain() + onTestDispatcherReset() + } + } + } + + open fun onTestDispatcherInitialized(testDispatcher: TestDispatcher) = Unit + + open fun onTestDispatcherReset() = Unit + + fun advanceUntilIdleWithIdlingResources() = runOnUIAwaitOnCurrent { + testDispatcher.advanceUntilIdleWithIdlingResources() + } + + fun advanceUntilIdle() = runOnUIAwaitOnCurrent { + testDispatcher.scheduler.advanceUntilIdle() + } + + fun advanceTimeBy(delayInMillis: Long) = runOnUIAwaitOnCurrent { + testDispatcher.scheduler.advanceTimeBy(delayInMillis) + } + + companion object { + fun TestDispatcher.advanceUntilIdleWithIdlingResources() { + scheduler.advanceUntilIdle() // advance until a request is sent + while (anyResourceIdling()) { // check if any request is in progress + awaitIdlingResources() // complete all requests and other idling resources + scheduler.advanceUntilIdle() // run coroutines after request is finished + } + scheduler.advanceUntilIdle() + } + } +} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/CompositeDisposable.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/CompositeDisposable.kt similarity index 85% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/CompositeDisposable.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/CompositeDisposable.kt index a3fbda4..ff3c139 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/CompositeDisposable.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/CompositeDisposable.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.testutils.idling +package org.fnives.test.showcase.android.testutil.synchronization.idlingresources class CompositeDisposable(disposable: List = emptyList()) : Disposable { diff --git a/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/Disposable.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/Disposable.kt new file mode 100644 index 0000000..32f97a1 --- /dev/null +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/Disposable.kt @@ -0,0 +1,6 @@ +package org.fnives.test.showcase.android.testutil.synchronization.idlingresources + +interface Disposable { + val isDisposed: Boolean + fun dispose() +} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/IdlingResourceDisposable.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/IdlingResourceDisposable.kt similarity index 69% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/IdlingResourceDisposable.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/IdlingResourceDisposable.kt index df5e2f3..2d2e934 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/IdlingResourceDisposable.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/IdlingResourceDisposable.kt @@ -1,9 +1,9 @@ -package org.fnives.test.showcase.testutils.idling +package org.fnives.test.showcase.android.testutil.synchronization.idlingresources import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingResource -internal class IdlingResourceDisposable(private val idlingResource: IdlingResource) : Disposable { +class IdlingResourceDisposable(private val idlingResource: IdlingResource) : Disposable { override var isDisposed: Boolean = false private set diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/IdlingResourcesHelper.kt similarity index 53% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/IdlingResourcesHelper.kt index 21996b5..e8288f7 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/IdlingResourcesHelper.kt @@ -1,12 +1,8 @@ -package org.fnives.test.showcase.testutils.idling +package org.fnives.test.showcase.android.testutil.synchronization.idlingresources -import android.os.Looper -import androidx.test.espresso.Espresso import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingResource -import androidx.test.espresso.matcher.ViewMatchers -import org.fnives.test.showcase.testutils.viewactions.LoopMainThreadFor -import org.fnives.test.showcase.testutils.viewactions.LoopMainThreadUntilIdle +import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor import java.util.concurrent.Executors // workaround, issue with idlingResources is tracked here https://github.com/robolectric/robolectric/issues/4807 @@ -41,20 +37,3 @@ private fun IdlingResource.awaitUntilIdle() { Thread.sleep(100L) } } - -fun loopMainThreadUntilIdleWithIdlingResources() { - Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadUntilIdle()) // advance until a request is sent - while (anyResourceIdling()) { // check if any request is in progress - awaitIdlingResources() // complete all requests and other idling resources - Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadUntilIdle()) // run coroutines after request is finished - } - Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadUntilIdle()) -} - -fun loopMainThreadFor(delay: Long) { - if (Looper.getMainLooper().isCurrentThread) { - Thread.sleep(200L) - } else { - Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(delay)) - } -} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/OkHttp3IdlingResource.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/OkHttp3IdlingResource.kt similarity index 94% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/OkHttp3IdlingResource.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/OkHttp3IdlingResource.kt index 14dd5a3..f0770ff 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/OkHttp3IdlingResource.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/idlingresources/OkHttp3IdlingResource.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.testutils.idling +package org.fnives.test.showcase.android.testutil.synchronization.idlingresources import androidx.annotation.CheckResult import androidx.annotation.NonNull @@ -40,7 +40,7 @@ class OkHttp3IdlingResource private constructor( * this instance using `Espresso.registerIdlingResources`. */ @CheckResult - @NonNull // Extra guards as a library. + @NonNull fun create(@NonNull name: String?, @NonNull client: OkHttpClient?): OkHttp3IdlingResource { if (name == null) throw NullPointerException("name == null") if (client == null) throw NullPointerException("client == null") diff --git a/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/mainThreadSynchronization.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/mainThreadSynchronization.kt new file mode 100644 index 0000000..25a3518 --- /dev/null +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/synchronization/mainThreadSynchronization.kt @@ -0,0 +1,35 @@ +package org.fnives.test.showcase.android.testutil.synchronization + +import android.os.Handler +import android.os.Looper +import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import org.fnives.test.showcase.android.testutil.viewaction.LoopMainThreadFor + +/** + * Runs the given action on the MainThread and blocks currentThread, until it is completed. + * + * It is safe to call this from the MainThread. + */ +fun runOnUIAwaitOnCurrent(action: () -> Unit) { + if (Looper.myLooper() === Looper.getMainLooper()) { + action() + } else { + val deferred = CompletableDeferred() + Handler(Looper.getMainLooper()).post { + action() + deferred.complete(Unit) + } + runBlocking { deferred.await() } + } +} + +fun loopMainThreadFor(delay: Long) { + if (Looper.getMainLooper().thread == Thread.currentThread()) { + Thread.sleep(200L) + } else { + Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(delay)) + } +} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/LoopMainThreadFor.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/LoopMainThreadFor.kt similarity index 59% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/LoopMainThreadFor.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/LoopMainThreadFor.kt index 7fe7451..a298b8c 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/LoopMainThreadFor.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/LoopMainThreadFor.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.testutils.viewactions +package org.fnives.test.showcase.android.testutil.viewaction import android.view.View import androidx.test.espresso.UiController @@ -15,13 +15,3 @@ class LoopMainThreadFor(private val delayInMillis: Long) : ViewAction { uiController.loopMainThreadForAtLeast(delayInMillis) } } - -class LoopMainThreadUntilIdle : ViewAction { - override fun getConstraints(): Matcher = Matchers.isA(View::class.java) - - override fun getDescription(): String = "loop MainThread for until Idle" - - override fun perform(uiController: UiController, view: View?) { - uiController.loopMainThreadUntilIdle() - } -} diff --git a/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/LoopMainThreadUntilIdle.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/LoopMainThreadUntilIdle.kt new file mode 100644 index 0000000..81fe989 --- /dev/null +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/LoopMainThreadUntilIdle.kt @@ -0,0 +1,17 @@ +package org.fnives.test.showcase.android.testutil.viewaction + +import android.view.View +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import org.hamcrest.Matcher +import org.hamcrest.Matchers + +class LoopMainThreadUntilIdle : ViewAction { + override fun getConstraints(): Matcher = Matchers.isA(View::class.java) + + override fun getDescription(): String = "loop MainThread for until Idle" + + override fun perform(uiController: UiController, view: View?) { + uiController.loopMainThreadUntilIdle() + } +} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/WithDrawable.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/imageview/WithDrawable.kt similarity index 89% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/WithDrawable.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/imageview/WithDrawable.kt index 79287c1..2303224 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/WithDrawable.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/imageview/WithDrawable.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.testutils.viewactions +package org.fnives.test.showcase.android.testutil.viewaction.imageview import android.content.res.ColorStateList import android.graphics.PorterDuff @@ -26,7 +26,7 @@ class WithDrawable( override fun matchesSafely(view: View): Boolean { val context = view.context val tintColor = tint?.let { ContextCompat.getColor(view.context, it) } - val expectedBitmap = context.getDrawable(id)?.apply { + val expectedBitmap = ContextCompat.getDrawable(context, id)?.apply { if (tintColor != null) { setTintList(ColorStateList.valueOf(tintColor)) setTintMode(tintMode) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/progressbar/ReplaceProgressBarDrawableToStatic.kt similarity index 91% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/progressbar/ReplaceProgressBarDrawableToStatic.kt index 44a6367..8293afa 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/progressbar/ReplaceProgressBarDrawableToStatic.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.testutils.viewactions +package org.fnives.test.showcase.android.testutil.viewaction.progressbar import android.graphics.Color import android.graphics.drawable.ColorDrawable diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/PullToRefresh.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/swiperefresh/PullToRefresh.kt similarity index 90% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/PullToRefresh.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/swiperefresh/PullToRefresh.kt index fa8a14c..73cecda 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/PullToRefresh.kt +++ b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/swiperefresh/PullToRefresh.kt @@ -1,11 +1,11 @@ -package org.fnives.test.showcase.testutils.viewactions +package org.fnives.test.showcase.android.testutil.viewaction.swiperefresh import android.view.View import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.listener import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction -import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent +import org.fnives.test.showcase.android.testutil.synchronization.runOnUIAwaitOnCurrent import org.hamcrest.BaseMatcher import org.hamcrest.CoreMatchers.isA import org.hamcrest.Description diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/SwipeRefreshLayoutExtension.kt b/test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/swiperefresh/SwipeRefreshLayoutExtension.kt similarity index 100% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/SwipeRefreshLayoutExtension.kt rename to test-util-android/src/main/java/org/fnives/test/showcase/android/testutil/viewaction/swiperefresh/SwipeRefreshLayoutExtension.kt diff --git a/test-util-junit5-android/.gitignore b/test-util-junit5-android/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/test-util-junit5-android/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/test-util-junit5-android/build.gradle b/test-util-junit5-android/build.gradle new file mode 100644 index 0000000..2809993 --- /dev/null +++ b/test-util-junit5-android/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 31 + + defaultConfig { + minSdk 21 + targetSdk 31 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + buildConfig = false + } + packagingOptions { + exclude 'META-INF/LGPL2.1' + exclude 'META-INF/AL2.0' + exclude 'META-INF/LICENSE.md' + exclude 'META-INF/LICENSE-notice.md' + } +} + +dependencies { + implementation "org.junit.jupiter:junit-jupiter-engine:$junit5_version" + implementation "androidx.arch.core:core-runtime:$arch_core_version" + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" +} + +ext.artifactId = "android-unit-junit5" +apply from: "../gradlescripts/deploy.aar.gradle" \ No newline at end of file diff --git a/test-util-junit5-android/consumer-rules.pro b/test-util-junit5-android/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/test-util-junit5-android/proguard-rules.pro b/test-util-junit5-android/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/test-util-junit5-android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/test-util-junit5-android/src/main/AndroidManifest.xml b/test-util-junit5-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..80e5dbb --- /dev/null +++ b/test-util-junit5-android/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/org/fnives/test/showcase/testutils/InstantExecutorExtension.kt b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/InstantExecutorExtension.kt similarity index 84% rename from app/src/test/java/org/fnives/test/showcase/testutils/InstantExecutorExtension.kt rename to test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/InstantExecutorExtension.kt index ca9d988..f64f387 100644 --- a/app/src/test/java/org/fnives/test/showcase/testutils/InstantExecutorExtension.kt +++ b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/InstantExecutorExtension.kt @@ -1,5 +1,6 @@ -package org.fnives.test.showcase.testutils +package org.fnives.test.showcase.android.testutil +import android.annotation.SuppressLint import androidx.arch.core.executor.ArchTaskExecutor import androidx.arch.core.executor.TaskExecutor import org.junit.jupiter.api.extension.AfterEachCallback @@ -11,9 +12,11 @@ import org.junit.jupiter.api.extension.ExtensionContext * * reference: https://developer.android.com/reference/androidx/arch/core/executor/testing/InstantTaskExecutorRule * - * A JUnit5 Extensions that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously. + * A JUnit5 Extensions that swaps the background executor used by the Architecture Components with a different + * one which executes each task synchronously. * You can use this extension for your host side tests that use Architecture Components. */ +@SuppressLint("RestrictedApi") class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback { override fun beforeEach(context: ExtensionContext?) { diff --git a/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/StandardTestMainDispatcher.kt b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/StandardTestMainDispatcher.kt new file mode 100644 index 0000000..9fda7c8 --- /dev/null +++ b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/StandardTestMainDispatcher.kt @@ -0,0 +1,30 @@ +package org.fnives.test.showcase.android.testutil + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher.Companion.testDispatcher + +/** + * Custom Junit5 Extension which replaces the main dispatcher with a Standard [TestDispatcher] + * + * One can access the test dispatcher via [testDispatcher] static getter. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class StandardTestMainDispatcher : TestMainDispatcher() { + + override var createdTestDispatcher: TestDispatcher? + get() = privateTestDispatcher + set(value) { + privateTestDispatcher = value + } + + override fun createDispatcher(): TestDispatcher = StandardTestDispatcher() + + companion object { + private var privateTestDispatcher: TestDispatcher? = null + val testDispatcher: TestDispatcher + get() = privateTestDispatcher + ?: throw IllegalStateException("StandardTestMainDispatcher is in afterEach State") + } +} diff --git a/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/TestMainDispatcher.kt b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/TestMainDispatcher.kt new file mode 100644 index 0000000..265e0ec --- /dev/null +++ b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/TestMainDispatcher.kt @@ -0,0 +1,32 @@ +package org.fnives.test.showcase.android.testutil + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext + +/** + * Custom Junit5 Extension which replaces the main dispatcher with a [TestDispatcher] + */ +@OptIn(ExperimentalCoroutinesApi::class) +abstract class TestMainDispatcher : BeforeEachCallback, AfterEachCallback { + + protected abstract var createdTestDispatcher: TestDispatcher? + + abstract fun createDispatcher(): TestDispatcher + + final override fun beforeEach(context: ExtensionContext?) { + val testDispatcher = createDispatcher() + createdTestDispatcher = testDispatcher + Dispatchers.setMain(testDispatcher) + } + + final override fun afterEach(context: ExtensionContext?) { + Dispatchers.resetMain() + createdTestDispatcher = null + } +} diff --git a/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/UnconfinedTestMainDispatcher.kt b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/UnconfinedTestMainDispatcher.kt new file mode 100644 index 0000000..5113237 --- /dev/null +++ b/test-util-junit5-android/src/main/java/org/fnives/test/showcase/android/testutil/UnconfinedTestMainDispatcher.kt @@ -0,0 +1,30 @@ +package org.fnives.test.showcase.android.testutil + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.fnives.test.showcase.android.testutil.UnconfinedTestMainDispatcher.Companion.testDispatcher + +/** + * Custom Junit5 Extension which replaces the main dispatcher with a Unconfined [TestDispatcher] + * + * One can access the test dispatcher via [testDispatcher] static getter. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class UnconfinedTestMainDispatcher : TestMainDispatcher() { + + override var createdTestDispatcher: TestDispatcher? + get() = privateTestDispatcher + set(value) { + privateTestDispatcher = value + } + + override fun createDispatcher(): TestDispatcher = UnconfinedTestDispatcher() + + companion object { + private var privateTestDispatcher: TestDispatcher? = null + val testDispatcher: TestDispatcher + get() = privateTestDispatcher + ?: throw IllegalStateException("StandardTestMainDispatcher is in afterEach State") + } +} diff --git a/test-util-shared-android/.gitignore b/test-util-shared-android/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/test-util-shared-android/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/test-util-shared-android/build.gradle b/test-util-shared-android/build.gradle new file mode 100644 index 0000000..bb2ffbc --- /dev/null +++ b/test-util-shared-android/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 31 + + defaultConfig { + minSdk 21 + targetSdk 31 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + buildConfig = false + } + packagingOptions { + exclude 'META-INF/LGPL2.1' + exclude 'META-INF/AL2.0' + exclude 'META-INF/LICENSE.md' + exclude 'META-INF/LICENSE-notice.md' + } +} + +dependencies { + implementation "androidx.room:room-testing:$room_version" + api project(':test-util-shared-robolectric') +} + +ext.artifactId = "shared-android" +apply from: "../gradlescripts/deploy.aar.gradle" \ No newline at end of file diff --git a/test-util-shared-android/consumer-rules.pro b/test-util-shared-android/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/test-util-shared-android/proguard-rules.pro b/test-util-shared-android/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/test-util-shared-android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/test-util-shared-android/src/main/AndroidManifest.xml b/test-util-shared-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..80e5dbb --- /dev/null +++ b/test-util-shared-android/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidMigrationTestRule.kt b/test-util-shared-android/src/main/java/org/fnives/test/showcase/android/testutil/AndroidMigrationTestRule.kt similarity index 88% rename from app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidMigrationTestRule.kt rename to test-util-shared-android/src/main/java/org/fnives/test/showcase/android/testutil/AndroidMigrationTestRule.kt index 882b3eb..33d0a0a 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidMigrationTestRule.kt +++ b/test-util-shared-android/src/main/java/org/fnives/test/showcase/android/testutil/AndroidMigrationTestRule.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.testutils.configuration +package org.fnives.test.showcase.android.testutil import android.app.Instrumentation import androidx.room.RoomDatabase @@ -10,6 +10,9 @@ import androidx.sqlite.db.SupportSQLiteOpenHelper import org.junit.runner.Description import org.junit.runners.model.Statement +/** + * Wrapper around [MigrationTestHelper] so it can be created in SharedTests, in both Robolectric and on Device. + */ class AndroidMigrationTestRule : SharedMigrationTestRule { private val migrationTestHelper: MigrationTestHelper @@ -35,8 +38,7 @@ class AndroidMigrationTestRule : SharedMigrationTestRule { specs: List, openFactory: SupportSQLiteOpenHelper.Factory ) { - migrationTestHelper = - MigrationTestHelper(instrumentation, databaseClass, specs, openFactory) + migrationTestHelper = MigrationTestHelper(instrumentation, databaseClass, specs, openFactory) } override fun apply(base: Statement, description: Description): Statement = diff --git a/test-util-shared-robolectric/.gitignore b/test-util-shared-robolectric/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/test-util-shared-robolectric/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/test-util-shared-robolectric/build.gradle b/test-util-shared-robolectric/build.gradle new file mode 100644 index 0000000..db2c58d --- /dev/null +++ b/test-util-shared-robolectric/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 31 + + defaultConfig { + minSdk 21 + targetSdk 31 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + buildConfig = false + } + packagingOptions { + exclude 'META-INF/LGPL2.1' + exclude 'META-INF/AL2.0' + exclude 'META-INF/LICENSE.md' + exclude 'META-INF/LICENSE-notice.md' + } +} + +dependencies { + implementation "androidx.room:room-testing:$room_version" + implementation "androidx.arch.core:core-testing:$arch_core_version" +} + +ext.artifactId = "shared-robolectric" +apply from: "../gradlescripts/deploy.aar.gradle" \ No newline at end of file diff --git a/test-util-shared-robolectric/src/main/AndroidManifest.xml b/test-util-shared-robolectric/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ed38c52 --- /dev/null +++ b/test-util-shared-robolectric/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/test-util-shared-robolectric/src/main/java/org/fnives/test/showcase/android/testutil/SharedMigrationTestRule.kt b/test-util-shared-robolectric/src/main/java/org/fnives/test/showcase/android/testutil/SharedMigrationTestRule.kt new file mode 100644 index 0000000..efee50e --- /dev/null +++ b/test-util-shared-robolectric/src/main/java/org/fnives/test/showcase/android/testutil/SharedMigrationTestRule.kt @@ -0,0 +1,29 @@ +package org.fnives.test.showcase.android.testutil + +import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import org.junit.rules.TestRule +import java.io.IOException + +/** + * Unifying API above [MigrationTestHelper][androidx.room.testing.MigrationTestHelper]. + * + * This is intended to be used in MigrationTests that are shared. Meaning the same test can run on both Device and via Robolectric. +*/ +interface SharedMigrationTestRule : TestRule { + + @Throws(IOException::class) + fun createDatabase(name: String, version: Int): SupportSQLiteDatabase + + @Throws(IOException::class) + fun runMigrationsAndValidate( + name: String, + version: Int, + validateDroppedTables: Boolean, + vararg migrations: Migration + ): SupportSQLiteDatabase + + fun closeWhenFinished(db: SupportSQLiteDatabase) + fun closeWhenFinished(db: RoomDatabase) +} diff --git a/test-util-shared-robolectric/src/main/java/org/fnives/test/showcase/android/testutil/SharedMigrationTestRuleFactory.kt b/test-util-shared-robolectric/src/main/java/org/fnives/test/showcase/android/testutil/SharedMigrationTestRuleFactory.kt new file mode 100644 index 0000000..cd49bdb --- /dev/null +++ b/test-util-shared-robolectric/src/main/java/org/fnives/test/showcase/android/testutil/SharedMigrationTestRuleFactory.kt @@ -0,0 +1,80 @@ +package org.fnives.test.showcase.android.testutil + +import android.app.Instrumentation +import androidx.room.RoomDatabase +import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SupportSQLiteOpenHelper +import org.fnives.test.showcase.android.testutil.robolectric.RobolectricMigrationTestRule + +inline fun SharedMigrationTestRule( + instrumentation: Instrumentation, +): SharedMigrationTestRule = + createAndroidClassOrRobolectric( + androidClassFactory = { androidClass -> + val constructor = androidClass.getConstructor( + Instrumentation::class.java, + Class::class.java + ) + constructor.newInstance(instrumentation, Database::class.java) as SharedMigrationTestRule + }, + robolectricFactory = { + RobolectricMigrationTestRule(instrumentation, Database::class.java) + } + ) + +inline fun SharedMigrationTestRule( + instrumentation: Instrumentation, + specs: List, +): SharedMigrationTestRule = + createAndroidClassOrRobolectric( + androidClassFactory = { androidClass -> + val constructor = androidClass.getConstructor( + Instrumentation::class.java, + Class::class.java, + List::class.java + ) + constructor.newInstance(instrumentation, Database::class.java, specs) as SharedMigrationTestRule + }, + robolectricFactory = { + RobolectricMigrationTestRule(instrumentation, Database::class.java, specs) + } + ) + +inline fun SharedMigrationTestRule( + instrumentation: Instrumentation, + specs: List, + openFactory: SupportSQLiteOpenHelper.Factory, +): SharedMigrationTestRule = + createAndroidClassOrRobolectric( + androidClassFactory = { androidClass -> + val constructor = androidClass.getConstructor( + Instrumentation::class.java, + Class::class.java, + List::class.java, + SupportSQLiteOpenHelper.Factory::class.java + ) + constructor.newInstance(instrumentation, Database::class.java, specs, openFactory) as SharedMigrationTestRule + }, + robolectricFactory = { + RobolectricMigrationTestRule(instrumentation, Database::class.java, specs, openFactory) + } + ) + +fun createAndroidClassOrRobolectric( + androidClassFactory: (Class<*>) -> Any, + robolectricFactory: () -> SharedMigrationTestRule, +): SharedMigrationTestRule { + val androidClass = getAndroidClass() + return if (androidClass == null) { + robolectricFactory() + } else { + androidClassFactory(androidClass) as SharedMigrationTestRule + } +} + +@Suppress("SwallowedException") +private fun getAndroidClass() = try { + Class.forName("org.fnives.test.showcase.android.testutil.AndroidMigrationTestRule") +} catch (classNotFoundException: ClassNotFoundException) { + null +} diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricMigrationTestHelper.java b/test-util-shared-robolectric/src/main/java/org/fnives/test/showcase/android/testutil/robolectric/RobolectricMigrationTestRule.java similarity index 97% rename from app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricMigrationTestHelper.java rename to test-util-shared-robolectric/src/main/java/org/fnives/test/showcase/android/testutil/robolectric/RobolectricMigrationTestRule.java index daf10f3..855d13a 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricMigrationTestHelper.java +++ b/test-util-shared-robolectric/src/main/java/org/fnives/test/showcase/android/testutil/robolectric/RobolectricMigrationTestRule.java @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.testutils.configuration; +package org.fnives.test.showcase.android.testutil.robolectric; import android.annotation.SuppressLint; import android.app.Instrumentation; @@ -32,6 +32,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.SupportSQLiteOpenHelper; import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; +import org.fnives.test.showcase.android.testutil.SharedMigrationTestRule; import org.jetbrains.annotations.NotNull; import org.junit.rules.TestWatcher; import org.junit.runner.Description; @@ -56,8 +57,9 @@ import java.util.Set; * * reference: https://github.com/robolectric/robolectric/issues/2065 */ -public class RobolectricMigrationTestHelper extends TestWatcher implements SharedMigrationTestRule { - private static final String TAG = "RobolectricMigrationTestHelper"; +@SuppressLint("RestrictedApi") +public class RobolectricMigrationTestRule extends TestWatcher implements SharedMigrationTestRule { + private static final String TAG = "RobolectricMigrationTR"; private final String mAssetsFolder; private final SupportSQLiteOpenHelper.Factory mOpenFactory; private List> mManagedDatabases = new ArrayList<>(); @@ -77,7 +79,7 @@ public class RobolectricMigrationTestHelper extends TestWatcher implements Share * @param instrumentation The instrumentation instance. * @param databaseClass The Database class to be tested. */ - public RobolectricMigrationTestHelper(@NonNull Instrumentation instrumentation, + public RobolectricMigrationTestRule(@NonNull Instrumentation instrumentation, @NonNull Class databaseClass) { this(instrumentation, databaseClass, new ArrayList<>(), new FrameworkSQLiteOpenHelperFactory()); @@ -96,7 +98,7 @@ public class RobolectricMigrationTestHelper extends TestWatcher implements Share * @param specs The list of available auto migration specs that will be provided to * Room at runtime. */ - public RobolectricMigrationTestHelper(@NonNull Instrumentation instrumentation, + public RobolectricMigrationTestRule(@NonNull Instrumentation instrumentation, @NonNull Class databaseClass, @NonNull List specs) { this(instrumentation, databaseClass, specs, new FrameworkSQLiteOpenHelperFactory()); @@ -116,7 +118,7 @@ public class RobolectricMigrationTestHelper extends TestWatcher implements Share * Room at runtime. * @param openFactory Factory class that allows creation of {@link SupportSQLiteOpenHelper} */ - public RobolectricMigrationTestHelper(@NonNull Instrumentation instrumentation, + public RobolectricMigrationTestRule(@NonNull Instrumentation instrumentation, @NonNull Class databaseClass, @NonNull List specs, @NonNull SupportSQLiteOpenHelper.Factory openFactory