Merge pull request #98 from fknives/issue#67

Issue#67
This commit is contained in:
Gergely Hegedis 2022-07-12 16:56:42 +03:00 committed by GitHub
commit 45bcd20b2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 1082 additions and 549 deletions

27
.github/workflows/release-package.yml vendored Normal file
View file

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

View file

@ -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. 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. 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:<latestVersion>" // test-util-junit5-android
testImplementation "org.fnives.android.testutil:shared-robolectric:<latestVersion>" // test-util-shared-robolectric
testImplementation "org.fnives.android.testutil:android:<latestVersion>" // test-util-android
androidTestImplementation "org.fnives.android.testutil:android:<latestVersion>" // test-util-android
androidTestImplementation "org.fnives.android.testutil:shared-android:<latestVersion>" // test-util-shared-android
```
## License ## License
[License file](./LICENSE) [License file](./LICENSE)

View file

@ -92,55 +92,25 @@ dependencies {
implementation "io.insert-koin:koin-android:$koin_version" implementation "io.insert-koin:koin-android:$koin_version"
implementation "io.insert-koin:koin-androidx-compose:$koin_version" implementation "io.insert-koin:koin-androidx-compose:$koin_version"
implementation "androidx.room:room-runtime:$androidx_room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$androidx_room_version" kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$androidx_room_version" implementation "androidx.room:room-ktx:$room_version"
implementation "io.coil-kt:coil:$coil_version" implementation "io.coil-kt:coil:$coil_version"
implementation "io.coil-kt:coil-compose:$coil_version" implementation "io.coil-kt:coil-compose:$coil_version"
implementation project(":core") implementation project(":core")
testImplementation "androidx.room:room-testing:$androidx_room_version" applyAppTestDependenciesTo(this)
testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" applyComposeTestDependenciesTo(this)
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"
// 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 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')) testImplementation testFixtures(project(':core'))
androidTestImplementation testFixtures(project(':core')) androidTestImplementation testFixtures(project(':core'))

View file

@ -11,12 +11,12 @@ import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4 import androidx.test.runner.AndroidJUnit4
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.fnives.test.showcase.R 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.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.testutils.storage.TestDatabaseInitialization
import org.fnives.test.showcase.ui.splash.SplashActivity import org.fnives.test.showcase.ui.splash.SplashActivity
import org.hamcrest.Description import org.hamcrest.Description

View file

@ -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<out RoomDatabase>
): SharedMigrationTestRule =
AndroidMigrationTestRule(
instrumentation,
databaseClass
)
override fun createSharedMigrationTestRule(
instrumentation: Instrumentation,
databaseClass: Class<out RoomDatabase>,
specs: List<AutoMigrationSpec>
): SharedMigrationTestRule =
AndroidMigrationTestRule(
instrumentation,
databaseClass,
specs
)
override fun createSharedMigrationTestRule(
instrumentation: Instrumentation,
databaseClass: Class<out RoomDatabase>,
specs: List<AutoMigrationSpec>,
openFactory: SupportSQLiteOpenHelper.Factory
): SharedMigrationTestRule =
AndroidMigrationTestRule(
instrumentation,
databaseClass,
specs,
openFactory,
)
}

View file

@ -1,7 +0,0 @@
package org.fnives.test.showcase.testutils.configuration
object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory =
AndroidMigrationTestRuleFactory
}

View file

@ -4,13 +4,13 @@ import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.R 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.compose.screen.AppNavigation
import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.idling.DispatcherTestRule import org.fnives.test.showcase.testutils.idling.DatabaseDispatcherTestRule
import org.fnives.test.showcase.testutils.idling.anyResourceIdling
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -27,7 +27,7 @@ class AuthComposeInstrumentedTest : KoinTest {
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule() private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
private val dispatcherTestRule = DispatcherTestRule() private val dispatcherTestRule = DatabaseDispatcherTestRule()
private lateinit var robot: ComposeLoginRobot private lateinit var robot: ComposeLoginRobot
private lateinit var navigationRobot: ComposeNavigationRobot private lateinit var navigationRobot: ComposeNavigationRobot

View file

@ -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<out RoomDatabase>
): SharedMigrationTestRule =
RobolectricMigrationTestHelper(
instrumentation,
databaseClass
)
override fun createSharedMigrationTestRule(
instrumentation: Instrumentation,
databaseClass: Class<out RoomDatabase>,
specs: List<AutoMigrationSpec>
): SharedMigrationTestRule =
RobolectricMigrationTestHelper(
instrumentation,
databaseClass,
specs
)
override fun createSharedMigrationTestRule(
instrumentation: Instrumentation,
databaseClass: Class<out RoomDatabase>,
specs: List<AutoMigrationSpec>,
openFactory: SupportSQLiteOpenHelper.Factory
): SharedMigrationTestRule =
RobolectricMigrationTestHelper(
instrumentation,
databaseClass,
specs,
openFactory
)
}

View file

@ -1,7 +0,0 @@
package org.fnives.test.showcase.testutils.configuration
object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory =
RobolectricMigrationTestHelperFactory
}

View file

@ -11,15 +11,15 @@ import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
import org.fnives.test.showcase.R 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.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper
import org.fnives.test.showcase.testutils.idling.CompositeDisposable import org.fnives.test.showcase.testutils.idling.DatabaseDispatcherTestRule.Companion.advanceUntilIdleWithIdlingResources
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.storage.TestDatabaseInitialization import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization
import org.fnives.test.showcase.ui.auth.AuthActivity import org.fnives.test.showcase.ui.auth.AuthActivity
import org.junit.After import org.junit.After

View file

@ -10,14 +10,13 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import org.fnives.test.showcase.R import org.fnives.test.showcase.R
import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationHelper import org.fnives.test.showcase.android.testutil.intent.notIntended
import org.fnives.test.showcase.testutils.viewactions.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.fnives.test.showcase.ui.home.MainActivity
import org.hamcrest.core.IsNot.not import org.hamcrest.core.IsNot.not
class RobolectricLoginRobot( class RobolectricLoginRobot {
private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationHelper()
) {
fun setUsername(username: String): RobolectricLoginRobot = apply { fun setUsername(username: String): RobolectricLoginRobot = apply {
onView(withId(R.id.user_edit_text)) onView(withId(R.id.user_edit_text))
@ -55,11 +54,11 @@ class RobolectricLoginRobot(
} }
fun assertErrorIsShown(@StringRes stringResID: Int) = apply { fun assertErrorIsShown(@StringRes stringResID: Int) = apply {
snackbarVerificationHelper.assertIsShownWithText(stringResID) assertSnackBarIsShownWithText(stringResID)
} }
fun assertErrorIsNotShown() = apply { fun assertErrorIsNotShown() = apply {
snackbarVerificationHelper.assertIsNotShown() assertSnackBarIsNotShown()
} }
fun assertNavigatedToHome() = apply { fun assertNavigatedToHome() = apply {

View file

@ -1,16 +1,14 @@
package org.fnives.test.showcase.storage.migration package org.fnives.test.showcase.storage.migration
import androidx.room.Room import androidx.room.Room
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking 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.LocalDatabase
import org.fnives.test.showcase.storage.favourite.FavouriteEntity import org.fnives.test.showcase.storage.favourite.FavouriteEntity
import org.fnives.test.showcase.storage.migation.Migration1To2 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.After
import org.junit.Assert import org.junit.Assert
import org.junit.Rule import org.junit.Rule
@ -28,11 +26,7 @@ import java.io.IOException
class MigrationToLatestInstrumentedTest { class MigrationToLatestInstrumentedTest {
@get:Rule @get:Rule
val helper: SharedMigrationTestRule = createSharedMigrationTestRule<LocalDatabase>( val helper = SharedMigrationTestRule<LocalDatabase>(instrumentation = InstrumentationRegistry.getInstrumentation())
InstrumentationRegistry.getInstrumentation(),
emptyList(),
FrameworkSQLiteOpenHelperFactory()
)
private fun getMigratedRoomDatabase(): LocalDatabase { private fun getMigratedRoomDatabase(): LocalDatabase {
val database: LocalDatabase = Room.databaseBuilder( val database: LocalDatabase = Room.databaseBuilder(

View file

@ -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 <reified DB : RoomDatabase> createSharedMigrationTestRule(
instrumentation: Instrumentation
): SharedMigrationTestRule =
SpecificTestConfigurationsFactory.createSharedMigrationTestRuleFactory()
.createSharedMigrationTestRule(
instrumentation,
DB::class.java
)
inline fun <reified DB : RoomDatabase> createSharedMigrationTestRule(
instrumentation: Instrumentation,
specs: List<AutoMigrationSpec>
): SharedMigrationTestRule =
SpecificTestConfigurationsFactory.createSharedMigrationTestRuleFactory()
.createSharedMigrationTestRule(
instrumentation,
DB::class.java,
specs
)
inline fun <reified DB : RoomDatabase> createSharedMigrationTestRule(
instrumentation: Instrumentation,
specs: List<AutoMigrationSpec>,
openFactory: SupportSQLiteOpenHelper.Factory
): SharedMigrationTestRule =
SpecificTestConfigurationsFactory.createSharedMigrationTestRuleFactory()
.createSharedMigrationTestRule(
instrumentation,
DB::class.java,
specs,
openFactory
)

View file

@ -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<out RoomDatabase>,
): SharedMigrationTestRule
fun createSharedMigrationTestRule(
instrumentation: Instrumentation,
databaseClass: Class<out RoomDatabase>,
specs: List<AutoMigrationSpec>
): SharedMigrationTestRule
fun createSharedMigrationTestRule(
instrumentation: Instrumentation,
databaseClass: Class<out RoomDatabase>,
specs: List<AutoMigrationSpec>,
openFactory: SupportSQLiteOpenHelper.Factory
): SharedMigrationTestRule
}

View file

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

View file

@ -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<Unit>()
Handler(Looper.getMainLooper()).post {
action()
deferred.complete(Unit)
}
runBlocking { deferred.await() }
}
}

View file

@ -3,14 +3,16 @@ package org.fnives.test.showcase.testutils.idling
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher 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.fnives.test.showcase.testutils.storage.TestDatabaseInitialization
import org.junit.rules.TestRule import org.junit.rules.TestRule
import org.junit.runner.Description import org.junit.runner.Description
import org.junit.runners.model.Statement import org.junit.runners.model.Statement
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class DispatcherTestRule : TestRule { class DatabaseDispatcherTestRule : TestRule {
private lateinit var testDispatcher: TestDispatcher private lateinit var testDispatcher: TestDispatcher

View file

@ -1,6 +0,0 @@
package org.fnives.test.showcase.testutils.idling
interface Disposable {
val isDisposed: Boolean
fun dispose()
}

View file

@ -1,58 +1,14 @@
package org.fnives.test.showcase.testutils.idling package org.fnives.test.showcase.testutils.idling
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher 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.fnives.test.showcase.testutils.storage.TestDatabaseInitialization
import org.junit.rules.TestRule import org.fnives.test.showcase.android.testutil.synchronization.MainDispatcherTestRule as LibMainDispatcherTestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherTestRule : TestRule { class MainDispatcherTestRule(useStandard: Boolean = true) : LibMainDispatcherTestRule(useStandard) {
private lateinit var testDispatcher: TestDispatcher override fun onTestDispatcherInitialized(testDispatcher: TestDispatcher) {
TestDatabaseInitialization.overwriteDatabaseInitialization(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()
}
} }
} }

View file

@ -3,6 +3,10 @@ package org.fnives.test.showcase.testutils.idling
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource
import okhttp3.OkHttpClient 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.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper
import org.junit.rules.TestRule import org.junit.rules.TestRule
import org.junit.runner.Description import org.junit.runner.Description

View file

@ -4,10 +4,10 @@ import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.runner.intent.IntentStubberRegistry 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.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule 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.auth.AuthActivity
import org.fnives.test.showcase.ui.home.HomeRobot import org.fnives.test.showcase.ui.home.HomeRobot
import org.fnives.test.showcase.ui.home.MainActivity import org.fnives.test.showcase.ui.home.MainActivity
@ -19,7 +19,7 @@ object SetupAuthenticationState : KoinTest {
fun setupLogin( fun setupLogin(
mainDispatcherTestRule: MainDispatcherTestRule, mainDispatcherTestRule: MainDispatcherTestRule,
mockServerScenarioSetup: MockServerScenarioSetup, mockServerScenarioSetup: MockServerScenarioSetup,
resetIntents: Boolean = true resetIntents: Boolean = true,
) { ) {
resetIntentsIfNeeded(resetIntents) { resetIntentsIfNeeded(resetIntents) {
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b")) mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b"))
@ -40,7 +40,7 @@ object SetupAuthenticationState : KoinTest {
fun setupLogout( fun setupLogout(
mainDispatcherTestRule: MainDispatcherTestRule, mainDispatcherTestRule: MainDispatcherTestRule,
resetIntents: Boolean = true resetIntents: Boolean = true,
) { ) {
resetIntentsIfNeeded(resetIntents) { resetIntentsIfNeeded(resetIntents) {
val activityScenario = ActivityScenario.launch(MainActivity::class.java) val activityScenario = ActivityScenario.launch(MainActivity::class.java)

View file

@ -19,11 +19,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import org.fnives.test.showcase.R 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.Content
import org.fnives.test.showcase.model.content.FavouriteContent 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.fnives.test.showcase.ui.auth.AuthActivity
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf

View file

@ -3,6 +3,8 @@ package org.fnives.test.showcase.ui.home
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4 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.model.content.FavouriteContent
import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.mockserver.scenario.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.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.idling.AsyncDiffUtilInstantTestRule import org.fnives.test.showcase.testutils.idling.AsyncDiffUtilInstantTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule 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.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -175,9 +174,6 @@ class MainActivityInstrumentedTest : KoinTest {
robot.swipeRefresh() robot.swipeRefresh()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loopMainThreadUntilIdleWithIdlingResources()
mainDispatcherTestRule.advanceTimeBy(1000L)
loopMainThreadFor(1000)
robot robot
.assertContainsError() .assertContainsError()

View file

@ -4,10 +4,10 @@ import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.R 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.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule 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.auth.AuthActivity
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before

View file

@ -14,15 +14,14 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import org.fnives.test.showcase.R import org.fnives.test.showcase.R
import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationHelper import org.fnives.test.showcase.android.testutil.intent.notIntended
import org.fnives.test.showcase.testutils.viewactions.ReplaceProgressBarDrawableToStatic import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsNotShown
import org.fnives.test.showcase.testutils.viewactions.notIntended 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.fnives.test.showcase.ui.home.MainActivity
import org.hamcrest.core.IsNot.not import org.hamcrest.core.IsNot.not
class LoginRobot( class LoginRobot {
private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationHelper()
) {
fun setupIntentResults() { fun setupIntentResults() {
Intents.intending(hasComponent(MainActivity::class.java.canonicalName)) Intents.intending(hasComponent(MainActivity::class.java.canonicalName))
@ -68,7 +67,7 @@ class LoginRobot(
} }
fun assertErrorIsShown(@StringRes stringResID: Int) = apply { fun assertErrorIsShown(@StringRes stringResID: Int) = apply {
snackbarVerificationHelper.assertIsShownWithText(stringResID) assertSnackBarIsShownWithText(stringResID)
} }
fun assertLoadingBeforeRequests() = apply { fun assertLoadingBeforeRequests() = apply {
@ -82,7 +81,7 @@ class LoginRobot(
} }
fun assertErrorIsNotShown() = apply { fun assertErrorIsNotShown() = apply {
snackbarVerificationHelper.assertIsNotShown() assertSnackBarIsNotShown()
} }
fun assertNavigatedToHome() = apply { fun assertNavigatedToHome() = apply {

View file

@ -8,14 +8,13 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import org.fnives.test.showcase.R import org.fnives.test.showcase.R
import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationHelper import org.fnives.test.showcase.android.testutil.intent.notIntended
import org.fnives.test.showcase.testutils.viewactions.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.fnives.test.showcase.ui.home.MainActivity
import org.hamcrest.core.IsNot import org.hamcrest.core.IsNot
class CodeKataSharedRobotTest( class CodeKataSharedRobotTest {
private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationHelper()
) {
fun setUsername(username: String): CodeKataSharedRobotTest = apply { fun setUsername(username: String): CodeKataSharedRobotTest = apply {
Espresso.onView(ViewMatchers.withId(R.id.user_edit_text)) Espresso.onView(ViewMatchers.withId(R.id.user_edit_text))
@ -53,11 +52,11 @@ class CodeKataSharedRobotTest(
} }
fun assertErrorIsShown(@StringRes stringResID: Int): CodeKataSharedRobotTest = apply { fun assertErrorIsShown(@StringRes stringResID: Int): CodeKataSharedRobotTest = apply {
snackbarVerificationHelper.assertIsShownWithText(stringResID) assertSnackBarIsShownWithText(stringResID)
} }
fun assertErrorIsNotShown(): CodeKataSharedRobotTest = apply { fun assertErrorIsNotShown(): CodeKataSharedRobotTest = apply {
snackbarVerificationHelper.assertIsNotShown() assertSnackBarIsNotShown()
} }
fun assertNavigatedToHome(): CodeKataSharedRobotTest = apply { fun assertNavigatedToHome(): CodeKataSharedRobotTest = apply {

View file

@ -4,9 +4,9 @@ import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4 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.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule 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.setupLogin
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogout import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogout
import org.junit.After import org.junit.After

View file

@ -5,7 +5,7 @@ import android.app.Instrumentation
import android.content.Intent import android.content.Intent
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.intent.matcher.IntentMatchers
import org.fnives.test.showcase.testutils.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.auth.AuthActivity
import org.fnives.test.showcase.ui.home.MainActivity import org.fnives.test.showcase.ui.home.MainActivity

View file

@ -1,8 +1,8 @@
package org.fnives.test.showcase.di package org.fnives.test.showcase.di
import android.content.Context import android.content.Context
import org.fnives.test.showcase.android.testutil.StandardTestMainDispatcher
import org.fnives.test.showcase.model.network.BaseUrl 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.auth.AuthViewModel
import org.fnives.test.showcase.ui.home.MainViewModel import org.fnives.test.showcase.ui.home.MainViewModel
import org.fnives.test.showcase.ui.splash.SplashViewModel 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.mock
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
@ExtendWith(TestMainDispatcher::class) @ExtendWith(StandardTestMainDispatcher::class)
class DITest : KoinTest { class DITest : KoinTest {
private val authViewModel by inject<AuthViewModel>() private val authViewModel by inject<AuthViewModel>()

View file

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

View file

@ -3,12 +3,12 @@ package org.fnives.test.showcase.ui.auth
import com.jraska.livedata.test import com.jraska.livedata.test
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking 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.core.login.LoginUseCase
import org.fnives.test.showcase.model.auth.LoginCredentials import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.shared.Answer 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.fnives.test.showcase.ui.shared.Event
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
@ -27,13 +27,13 @@ import org.mockito.kotlin.whenever
import java.util.stream.Stream import java.util.stream.Stream
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal class AuthViewModelTest { internal class AuthViewModelTest {
private lateinit var sut: AuthViewModel private lateinit var sut: AuthViewModel
private lateinit var mockLoginUseCase: LoginUseCase private lateinit var mockLoginUseCase: LoginUseCase
private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
@ -169,7 +169,7 @@ internal class AuthViewModelTest {
@ParameterizedTest(name = "GIVEN answer success loginStatus {0} WHEN login called THEN error {1} is shown") @ParameterizedTest(name = "GIVEN answer success loginStatus {0} WHEN login called THEN error {1} is shown")
fun invalidStatusResultsInErrorState( fun invalidStatusResultsInErrorState(
loginStatus: LoginStatus, loginStatus: LoginStatus,
errorType: AuthViewModel.ErrorType errorType: AuthViewModel.ErrorType,
) { ) {
runBlocking { runBlocking {
whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Success(loginStatus)) whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Success(loginStatus))

View file

@ -1,22 +1,22 @@
package org.fnives.test.showcase.ui.auth package org.fnives.test.showcase.ui.auth
import kotlinx.coroutines.ExperimentalCoroutinesApi 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.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.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class CodeKataAuthViewModel { class CodeKataAuthViewModel {
private lateinit var sut: AuthViewModel private lateinit var sut: AuthViewModel
private lateinit var mockLoginUseCase: LoginUseCase private lateinit var mockLoginUseCase: LoginUseCase
private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {

View file

@ -4,6 +4,8 @@ import com.jraska.livedata.test
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking 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.AddContentToFavouriteUseCase
import org.fnives.test.showcase.core.content.FetchContentUseCase import org.fnives.test.showcase.core.content.FetchContentUseCase
import org.fnives.test.showcase.core.content.GetAllContentUseCase 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.FavouriteContent
import org.fnives.test.showcase.model.content.ImageUrl import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.model.shared.Resource 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.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mockito.verifyNoInteractions
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.times import org.mockito.kotlin.times
import org.mockito.kotlin.verify import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal class MainViewModelTest { internal class MainViewModelTest {
@ -39,7 +39,7 @@ internal class MainViewModelTest {
private lateinit var mockFetchContentUseCase: FetchContentUseCase private lateinit var mockFetchContentUseCase: FetchContentUseCase
private lateinit var mockAddContentToFavouriteUseCase: AddContentToFavouriteUseCase private lateinit var mockAddContentToFavouriteUseCase: AddContentToFavouriteUseCase
private lateinit var mockRemoveContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase private lateinit var mockRemoveContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase
private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {

View file

@ -2,9 +2,9 @@ package org.fnives.test.showcase.ui.splash
import com.jraska.livedata.test import com.jraska.livedata.test
import kotlinx.coroutines.ExperimentalCoroutinesApi 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.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.fnives.test.showcase.ui.shared.Event
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
@ -14,13 +14,13 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal class SplashViewModelTest { internal class SplashViewModelTest {
private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase
private lateinit var sut: SplashViewModel private lateinit var sut: SplashViewModel
private val testScheduler get() = TestMainDispatcher.testDispatcher.scheduler private val testScheduler get() = StandardTestMainDispatcher.testDispatcher.scheduler
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {

View file

@ -22,6 +22,14 @@ allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
google() 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/detekt.config.gradle'
apply from: 'gradlescripts/ktlint.gradle' apply from: 'gradlescripts/ktlint.gradle'
apply from: 'gradlescripts/lint.gradle' apply from: 'gradlescripts/lint.gradle'
apply from: 'gradlescripts/testoptions.gradle' apply from: 'gradlescripts/testoptions.gradle'
apply from: 'gradlescripts/test.tasks.gradle'
apply from: 'gradlescripts/testdependencies.gradle'

View file

@ -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`. 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: To add this to our TestClass we need to do the following:
```kotlin ```kotlin
@ExtendWith(InstantExecutorExtension::class, TestMainDispatcher::class) @ExtendWith(InstantExecutorExtension::class, StandardTestMainDispatcher::class)
class CodeKataSplashViewModelTest { class CodeKataSplashViewModelTest {
``` ```
@ -41,7 +41,7 @@ Next let's set up our System Under Test as usual:
```kotlin ```kotlin
private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase
private lateinit var sut: SplashViewModel 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 @BeforeEach
fun setUp() { 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. 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 ```kotlin
testScheduler.advanceTimeBy(501) testScheduler.advanceTimeBy(501)

View file

@ -20,15 +20,7 @@ dependencies {
api project(":model") api project(":model")
implementation project(":network") implementation project(":network")
testImplementation "io.insert-koin:koin-test-junit5:$koin_version" applyCoreTestDependenciesTo(this)
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"
testImplementation project(':mockserver') testImplementation project(':mockserver')
testFixturesApi testFixtures(project(':network')) testFixturesApi testFixtures(project(':network'))

View file

@ -18,4 +18,5 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=false android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
android.disableAutomaticComponentCreation=true

View file

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

View file

@ -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'
}

View file

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

View file

@ -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'
} }

View file

@ -1,11 +1,11 @@
project.ext { project.ext {
androidx_core_version = "1.7.0" androidx_core_version = "1.8.0"
androidx_appcompat_version = "1.4.1" 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_constraintlayout_version = "2.1.3"
androidx_livedata_version = "2.4.0" androidx_livedata_version = "2.4.0"
androidx_swiperefreshlayout_version = "1.1.0" androidx_swiperefreshlayout_version = "1.1.0"
androidx_room_version = "2.4.1" room_version = "2.4.2"
activity_ktx_version = "1.4.0" activity_ktx_version = "1.4.0"
androidx_navigation = "2.4.0" androidx_navigation = "2.4.0"
@ -14,21 +14,23 @@ project.ext {
androidx_compose_constraintlayout_version = "1.0.0" androidx_compose_constraintlayout_version = "1.0.0"
coroutines_version = "1.6.0" coroutines_version = "1.6.0"
turbine_version = "0.7.0"
koin_version = "3.1.2" koin_version = "3.1.2"
coil_version = "1.4.0" coil_version = "1.4.0"
retrofit_version = "2.9.0" retrofit_version = "2.9.0"
okhttp_version = "4.9.3" okhttp_version = "4.9.3"
moshi_version = "1.13.0" moshi_version = "1.13.0"
testing_androidx_code_version = "1.4.0" // testing versions
testing_androidx_junit_version = "1.1.3" turbine_version = "0.7.0"
testing_androidx_arch_core_version = "2.1.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_livedata_version = "1.2.0"
testing_kotlin_mockito_version = "4.0.0" mockito_version = "4.0.0"
testing_junit5_version = "5.7.0" junit5_version = "5.7.0"
testing_json_assert_version = "1.5.0" json_assert_version = "1.5.0"
testing_junit4_version = "4.13.2" junit4_version = "4.13.2"
testing_robolectric_version = "4.7" robolectric_version = "4.7"
testing_espresso_version = "3.4.0" espresso_version = "3.4.0"
hamcrest_version = "2.2"
} }

View file

@ -14,7 +14,7 @@ dependencies {
api "com.squareup.okhttp3:mockwebserver:$okhttp_version" api "com.squareup.okhttp3:mockwebserver:$okhttp_version"
api "com.squareup.okhttp3:okhttp-tls:$okhttp_version" api "com.squareup.okhttp3:okhttp-tls:$okhttp_version"
implementation "org.skyscreamer:jsonassert:$testing_json_assert_version" implementation "org.skyscreamer:jsonassert:$json_assert_version"
implementation "junit:junit:$testing_junit4_version" implementation "junit:junit:$junit4_version"
} }

View file

@ -24,12 +24,9 @@ dependencies {
api project(":model") api project(":model")
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" applyNetworkTestDependenciesTo(this)
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"
testFixturesApi project(':mockserver') 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" testFixturesApi "io.insert-koin:koin-test-junit5:$koin_version"
} }

View file

@ -1,6 +1,10 @@
rootProject.name = "TestShowCase"
include ':mockserver' include ':mockserver'
include ':model' include ':model'
include ':core' include ':core'
include ':network' include ':network'
include ':app' include ':app'
rootProject.name = "TestShowCase" include ':test-util-shared-android'
include ':test-util-shared-robolectric'
include ':test-util-android'
include ':test-util-junit5-android'

1
test-util-android/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

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

View file

21
test-util-android/proguard-rules.pro vendored Normal file
View file

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

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.fnives.test.showcase.android.testutil">
</manifest>

View file

@ -1,19 +1,24 @@
package org.fnives.test.showcase.testutils package org.fnives.test.showcase.android.testutil.activity
import android.app.Activity import android.app.Activity
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
/**
* Workaround for issue: https://github.com/android/android-test/issues/676.
*
* Call this instead of ActivityScenario.close().
*/
fun <T : Activity> ActivityScenario<T>.safeClose() { fun <T : Activity> ActivityScenario<T>.safeClose() {
workaroundForActivityScenarioCLoseLockingUp() workaroundForActivityScenarioCLoseLockingUp()
close() 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 * 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. * 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. * As soon as that issue is closed, this should be removed as well.
*/ */

View file

@ -1,10 +1,11 @@
package org.fnives.test.showcase.testutils.viewactions package org.fnives.test.showcase.android.testutil.intent
import android.content.Intent import android.content.Intent
import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.Intents.intended
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.StringDescription import org.hamcrest.StringDescription
@Suppress("SwallowedException")
fun notIntended(matcher: Matcher<Intent>) { fun notIntended(matcher: Matcher<Intent>) {
try { try {
intended(matcher) intended(matcher)

View file

@ -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 android.view.View
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
@ -8,22 +9,25 @@ import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import com.google.android.material.R
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.Matchers import org.hamcrest.Matchers
import com.google.android.material.R as MaterialR
class SnackbarVerificationHelper { object SnackbarVerificationHelper {
fun assertIsShownWithText(@StringRes stringResID: Int) { @SuppressLint("RestrictedApi")
Espresso.onView(ViewMatchers.withId(R.id.snackbar_text)) fun assertSnackBarIsShownWithText(@StringRes stringResID: Int, doDismiss: Boolean = true) {
Espresso.onView(ViewMatchers.withId(MaterialR.id.snackbar_text))
.check(ViewAssertions.matches(ViewMatchers.withText(stringResID))) .check(ViewAssertions.matches(ViewMatchers.withText(stringResID)))
Espresso.onView(ViewMatchers.isAssignableFrom(Snackbar.SnackbarLayout::class.java)).perform(ViewActions.swipeRight()) if (doDismiss) {
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainUntilSnackbarDismissed()) Espresso.onView(ViewMatchers.isAssignableFrom(Snackbar.SnackbarLayout::class.java)).perform(ViewActions.swipeRight())
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainUntilSnackbarDismissed())
}
} }
fun assertIsNotShown() { fun assertSnackBarIsNotShown() {
Espresso.onView(ViewMatchers.withId(R.id.snackbar_text)).check(ViewAssertions.doesNotExist()) Espresso.onView(ViewMatchers.withId(MaterialR.id.snackbar_text)).check(ViewAssertions.doesNotExist())
} }
class LoopMainUntilSnackbarDismissed : ViewAction { class LoopMainUntilSnackbarDismissed : ViewAction {

View file

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

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.testutils.idling package org.fnives.test.showcase.android.testutil.synchronization.idlingresources
class CompositeDisposable(disposable: List<Disposable> = emptyList()) : Disposable { class CompositeDisposable(disposable: List<Disposable> = emptyList()) : Disposable {

View file

@ -0,0 +1,6 @@
package org.fnives.test.showcase.android.testutil.synchronization.idlingresources
interface Disposable {
val isDisposed: Boolean
fun dispose()
}

View file

@ -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.IdlingRegistry
import androidx.test.espresso.IdlingResource 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 override var isDisposed: Boolean = false
private set private set

View file

@ -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.IdlingRegistry
import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource
import androidx.test.espresso.matcher.ViewMatchers import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor
import org.fnives.test.showcase.testutils.viewactions.LoopMainThreadFor
import org.fnives.test.showcase.testutils.viewactions.LoopMainThreadUntilIdle
import java.util.concurrent.Executors import java.util.concurrent.Executors
// workaround, issue with idlingResources is tracked here https://github.com/robolectric/robolectric/issues/4807 // 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) 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))
}
}

View file

@ -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.CheckResult
import androidx.annotation.NonNull import androidx.annotation.NonNull
@ -40,7 +40,7 @@ class OkHttp3IdlingResource private constructor(
* this instance using `Espresso.registerIdlingResources`. * this instance using `Espresso.registerIdlingResources`.
*/ */
@CheckResult @CheckResult
@NonNull // Extra guards as a library. @NonNull
fun create(@NonNull name: String?, @NonNull client: OkHttpClient?): OkHttp3IdlingResource { fun create(@NonNull name: String?, @NonNull client: OkHttpClient?): OkHttp3IdlingResource {
if (name == null) throw NullPointerException("name == null") if (name == null) throw NullPointerException("name == null")
if (client == null) throw NullPointerException("client == null") if (client == null) throw NullPointerException("client == null")

View file

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

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.testutils.viewactions package org.fnives.test.showcase.android.testutil.viewaction
import android.view.View import android.view.View
import androidx.test.espresso.UiController import androidx.test.espresso.UiController
@ -15,13 +15,3 @@ class LoopMainThreadFor(private val delayInMillis: Long) : ViewAction {
uiController.loopMainThreadForAtLeast(delayInMillis) uiController.loopMainThreadForAtLeast(delayInMillis)
} }
} }
class LoopMainThreadUntilIdle : ViewAction {
override fun getConstraints(): Matcher<View> = Matchers.isA(View::class.java)
override fun getDescription(): String = "loop MainThread for until Idle"
override fun perform(uiController: UiController, view: View?) {
uiController.loopMainThreadUntilIdle()
}
}

View file

@ -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<View> = Matchers.isA(View::class.java)
override fun getDescription(): String = "loop MainThread for until Idle"
override fun perform(uiController: UiController, view: View?) {
uiController.loopMainThreadUntilIdle()
}
}

View file

@ -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.content.res.ColorStateList
import android.graphics.PorterDuff import android.graphics.PorterDuff
@ -26,7 +26,7 @@ class WithDrawable(
override fun matchesSafely(view: View): Boolean { override fun matchesSafely(view: View): Boolean {
val context = view.context val context = view.context
val tintColor = tint?.let { ContextCompat.getColor(view.context, it) } 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) { if (tintColor != null) {
setTintList(ColorStateList.valueOf(tintColor)) setTintList(ColorStateList.valueOf(tintColor))
setTintMode(tintMode) setTintMode(tintMode)

View file

@ -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.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable

View file

@ -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 android.view.View
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.listener import androidx.swiperefreshlayout.widget.listener
import androidx.test.espresso.UiController import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction 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.BaseMatcher
import org.hamcrest.CoreMatchers.isA import org.hamcrest.CoreMatchers.isA
import org.hamcrest.Description import org.hamcrest.Description

1
test-util-junit5-android/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

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

View file

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

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.fnives.test.showcase.android.testutil">
</manifest>

View file

@ -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.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor import androidx.arch.core.executor.TaskExecutor
import org.junit.jupiter.api.extension.AfterEachCallback 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 * 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. * You can use this extension for your host side tests that use Architecture Components.
*/ */
@SuppressLint("RestrictedApi")
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback { class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) { override fun beforeEach(context: ExtensionContext?) {

View file

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

View file

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

View file

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

1
test-util-shared-android/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

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

View file

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

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.fnives.test.showcase.android.testutil">
</manifest>

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.testutils.configuration package org.fnives.test.showcase.android.testutil
import android.app.Instrumentation import android.app.Instrumentation
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
@ -10,6 +10,9 @@ import androidx.sqlite.db.SupportSQLiteOpenHelper
import org.junit.runner.Description import org.junit.runner.Description
import org.junit.runners.model.Statement 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 { class AndroidMigrationTestRule : SharedMigrationTestRule {
private val migrationTestHelper: MigrationTestHelper private val migrationTestHelper: MigrationTestHelper
@ -35,8 +38,7 @@ class AndroidMigrationTestRule : SharedMigrationTestRule {
specs: List<AutoMigrationSpec>, specs: List<AutoMigrationSpec>,
openFactory: SupportSQLiteOpenHelper.Factory openFactory: SupportSQLiteOpenHelper.Factory
) { ) {
migrationTestHelper = migrationTestHelper = MigrationTestHelper(instrumentation, databaseClass, specs, openFactory)
MigrationTestHelper(instrumentation, databaseClass, specs, openFactory)
} }
override fun apply(base: Statement, description: Description): Statement = override fun apply(base: Statement, description: Description): Statement =

View file

@ -0,0 +1 @@
/build

View file

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

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.fnives.test.showcase.android.testutil.robolectric">
</manifest>

View file

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

View file

@ -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 <reified Database : RoomDatabase> 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 <reified Database : RoomDatabase> SharedMigrationTestRule(
instrumentation: Instrumentation,
specs: List<AutoMigrationSpec>,
): 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 <reified Database : RoomDatabase> SharedMigrationTestRule(
instrumentation: Instrumentation,
specs: List<AutoMigrationSpec>,
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
}

View file

@ -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.annotation.SuppressLint;
import android.app.Instrumentation; import android.app.Instrumentation;
@ -32,6 +32,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper; import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
import org.fnives.test.showcase.android.testutil.SharedMigrationTestRule;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.junit.rules.TestWatcher; import org.junit.rules.TestWatcher;
import org.junit.runner.Description; import org.junit.runner.Description;
@ -56,8 +57,9 @@ import java.util.Set;
* *
* reference: https://github.com/robolectric/robolectric/issues/2065 * reference: https://github.com/robolectric/robolectric/issues/2065
*/ */
public class RobolectricMigrationTestHelper extends TestWatcher implements SharedMigrationTestRule { @SuppressLint("RestrictedApi")
private static final String TAG = "RobolectricMigrationTestHelper"; public class RobolectricMigrationTestRule extends TestWatcher implements SharedMigrationTestRule {
private static final String TAG = "RobolectricMigrationTR";
private final String mAssetsFolder; private final String mAssetsFolder;
private final SupportSQLiteOpenHelper.Factory mOpenFactory; private final SupportSQLiteOpenHelper.Factory mOpenFactory;
private List<WeakReference<SupportSQLiteDatabase>> mManagedDatabases = new ArrayList<>(); private List<WeakReference<SupportSQLiteDatabase>> mManagedDatabases = new ArrayList<>();
@ -77,7 +79,7 @@ public class RobolectricMigrationTestHelper extends TestWatcher implements Share
* @param instrumentation The instrumentation instance. * @param instrumentation The instrumentation instance.
* @param databaseClass The Database class to be tested. * @param databaseClass The Database class to be tested.
*/ */
public RobolectricMigrationTestHelper(@NonNull Instrumentation instrumentation, public RobolectricMigrationTestRule(@NonNull Instrumentation instrumentation,
@NonNull Class<? extends RoomDatabase> databaseClass) { @NonNull Class<? extends RoomDatabase> databaseClass) {
this(instrumentation, databaseClass, new ArrayList<>(), this(instrumentation, databaseClass, new ArrayList<>(),
new FrameworkSQLiteOpenHelperFactory()); 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 * @param specs The list of available auto migration specs that will be provided to
* Room at runtime. * Room at runtime.
*/ */
public RobolectricMigrationTestHelper(@NonNull Instrumentation instrumentation, public RobolectricMigrationTestRule(@NonNull Instrumentation instrumentation,
@NonNull Class<? extends RoomDatabase> databaseClass, @NonNull Class<? extends RoomDatabase> databaseClass,
@NonNull List<AutoMigrationSpec> specs) { @NonNull List<AutoMigrationSpec> specs) {
this(instrumentation, databaseClass, specs, new FrameworkSQLiteOpenHelperFactory()); this(instrumentation, databaseClass, specs, new FrameworkSQLiteOpenHelperFactory());
@ -116,7 +118,7 @@ public class RobolectricMigrationTestHelper extends TestWatcher implements Share
* Room at runtime. * Room at runtime.
* @param openFactory Factory class that allows creation of {@link SupportSQLiteOpenHelper} * @param openFactory Factory class that allows creation of {@link SupportSQLiteOpenHelper}
*/ */
public RobolectricMigrationTestHelper(@NonNull Instrumentation instrumentation, public RobolectricMigrationTestRule(@NonNull Instrumentation instrumentation,
@NonNull Class<? extends RoomDatabase> databaseClass, @NonNull Class<? extends RoomDatabase> databaseClass,
@NonNull List<AutoMigrationSpec> specs, @NonNull List<AutoMigrationSpec> specs,
@NonNull SupportSQLiteOpenHelper.Factory openFactory @NonNull SupportSQLiteOpenHelper.Factory openFactory