diff --git a/app/src/androidTest/java/org/fnives/test/showcase/endtoend/LoginLogoutEndToEndTest.kt b/app/src/androidTest/java/org/fnives/test/showcase/endtoend/LoginLogoutEndToEndTest.kt index f03fde9..911f845 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/endtoend/LoginLogoutEndToEndTest.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/endtoend/LoginLogoutEndToEndTest.kt @@ -12,12 +12,12 @@ import androidx.test.runner.AndroidJUnit4 import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.fnives.test.showcase.R import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper -import org.fnives.test.showcase.storage.database.DatabaseInitialization import org.fnives.test.showcase.testutils.idling.CompositeDisposable import org.fnives.test.showcase.testutils.idling.Disposable import org.fnives.test.showcase.testutils.idling.IdlingResourceDisposable import org.fnives.test.showcase.testutils.idling.OkHttp3IdlingResource import org.fnives.test.showcase.testutils.idling.loopMainThreadFor +import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization import org.fnives.test.showcase.ui.splash.SplashActivity import org.hamcrest.Description import org.hamcrest.Matcher @@ -45,7 +45,7 @@ class LoginLogoutEndToEndTest { @Before fun before() { /** Needed to add the dispatcher to the Database */ - DatabaseInitialization.dispatcher = UnconfinedTestDispatcher() + TestDatabaseInitialization.overwriteDatabaseInitialization(UnconfinedTestDispatcher()) /** Needed to register the Okhttp as Idling resource, so Espresso actually waits for the response.*/ val idlingResources = NetworkTestConfigurationHelper.getOkHttpClients() diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplInstrumentedTest.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplInstrumentedTest.kt index 7ee1696..765e41b 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplInstrumentedTest.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplInstrumentedTest.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.test.runTest import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage import org.fnives.test.showcase.model.content.ContentId -import org.fnives.test.showcase.storage.database.DatabaseInitialization +import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization import org.junit.After import org.junit.Assert import org.junit.Before @@ -36,7 +36,7 @@ internal class FavouriteContentLocalStorageImplInstrumentedTest( @Before fun setUp() { testDispatcher = StandardTestDispatcher() - DatabaseInitialization.dispatcher = testDispatcher + TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatcher) sut = favouriteContentLocalStorageFactory() } diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricAuthActivityInstrumentedTest.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricAuthActivityInstrumentedTest.kt index 5ec50d1..01e6399 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricAuthActivityInstrumentedTest.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/ui/RobolectricAuthActivityInstrumentedTest.kt @@ -14,13 +14,13 @@ import org.fnives.test.showcase.R import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper -import org.fnives.test.showcase.storage.database.DatabaseInitialization import org.fnives.test.showcase.testutils.idling.CompositeDisposable import org.fnives.test.showcase.testutils.idling.Disposable import org.fnives.test.showcase.testutils.idling.IdlingResourceDisposable import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule.Companion.advanceUntilIdleWithIdlingResources import org.fnives.test.showcase.testutils.idling.OkHttp3IdlingResource import org.fnives.test.showcase.testutils.safeClose +import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization import org.fnives.test.showcase.ui.auth.AuthActivity import org.junit.After import org.junit.Before @@ -45,7 +45,7 @@ class RobolectricAuthActivityInstrumentedTest : KoinTest { val dispatcher = StandardTestDispatcher() Dispatchers.setMain(dispatcher) testDispatcher = dispatcher - DatabaseInitialization.dispatcher = dispatcher + TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher) mockServerScenarioSetup = NetworkTestConfigurationHelper.startWithHTTPSMockWebServer() diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/storage/database/DatabaseInitialization.kt b/app/src/sharedTest/java/org/fnives/test/showcase/storage/database/DatabaseInitialization.kt deleted file mode 100644 index 9a6277b..0000000 --- a/app/src/sharedTest/java/org/fnives/test/showcase/storage/database/DatabaseInitialization.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.fnives.test.showcase.storage.database - -import android.content.Context -import androidx.room.Room -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.asExecutor -import org.fnives.test.showcase.storage.LocalDatabase - -object DatabaseInitialization { - - lateinit var dispatcher: CoroutineDispatcher - - fun create(context: Context): LocalDatabase { - val executor = dispatcher.asExecutor() - return Room.inMemoryDatabaseBuilder(context, LocalDatabase::class.java) - .setTransactionExecutor(executor) - .setQueryExecutor(executor) - .allowMainThreadQueries() - .build() - } -} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DispatcherTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DispatcherTestRule.kt index 27e675d..8d2f0bc 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DispatcherTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/DispatcherTestRule.kt @@ -3,8 +3,8 @@ package org.fnives.test.showcase.testutils.idling import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher -import org.fnives.test.showcase.storage.database.DatabaseInitialization import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent +import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @@ -20,7 +20,7 @@ class DispatcherTestRule : TestRule { override fun evaluate() { val dispatcher = StandardTestDispatcher() testDispatcher = dispatcher - DatabaseInitialization.dispatcher = dispatcher + TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher) base.evaluate() } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt index 6553624..de21704 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt @@ -6,8 +6,8 @@ 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.storage.database.DatabaseInitialization import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent +import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @@ -24,7 +24,7 @@ class MainDispatcherTestRule : TestRule { val dispatcher = StandardTestDispatcher() Dispatchers.setMain(dispatcher) testDispatcher = dispatcher - DatabaseInitialization.dispatcher = dispatcher + TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher) try { base.evaluate() } finally { diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/storage/TestDatabaseInitialization.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/storage/TestDatabaseInitialization.kt new file mode 100644 index 0000000..7154ad1 --- /dev/null +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/storage/TestDatabaseInitialization.kt @@ -0,0 +1,31 @@ +package org.fnives.test.showcase.testutils.storage + +import android.content.Context +import androidx.room.Room +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.asExecutor +import org.fnives.test.showcase.storage.LocalDatabase +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.loadKoinModules +import org.koin.dsl.module + +/** + * Reloads the Database Koin module, so it uses the inMemory database with the switched out Executors. + */ +object TestDatabaseInitialization { + + fun create(context: Context, dispatcher: CoroutineDispatcher): LocalDatabase { + val executor = dispatcher.asExecutor() + return Room.inMemoryDatabaseBuilder(context, LocalDatabase::class.java) + .setTransactionExecutor(executor) + .setQueryExecutor(executor) + .allowMainThreadQueries() + .build() + } + + fun overwriteDatabaseInitialization(dispatcher: CoroutineDispatcher) { + loadKoinModules(module { + single { create(androidContext(), dispatcher) } + }) + } +} \ No newline at end of file diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/codekata/rule/dispatcher/PlainMainDispatcherRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/codekata/rule/dispatcher/PlainMainDispatcherRule.kt index 181f351..bb3f10d 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/codekata/rule/dispatcher/PlainMainDispatcherRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/codekata/rule/dispatcher/PlainMainDispatcherRule.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.fnives.test.showcase.storage.database.DatabaseInitialization +import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @@ -28,7 +29,7 @@ class PlainMainDispatcherRule(private val useStandard: Boolean = true) : TestRul try { val dispatcher = if (useStandard) StandardTestDispatcher() else UnconfinedTestDispatcher() Dispatchers.setMain(dispatcher) - DatabaseInitialization.dispatcher = dispatcher + TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher) _testDispatcher = dispatcher base.evaluate() } finally { diff --git a/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt b/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt index 42592a5..77beff4 100644 --- a/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt +++ b/app/src/test/java/org/fnives/test/showcase/testutils/TestMainDispatcher.kt @@ -6,14 +6,13 @@ 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.storage.database.DatabaseInitialization 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 [DatabaseInitialization]'s dispatcher and main dispatcher with a [TestDispatcher] + * Custom Junit5 Extension which replaces the main dispatcher with a [TestDispatcher] * * One can access the test dispatcher via [testDispatcher] static getter. */ @@ -23,7 +22,6 @@ class TestMainDispatcher : BeforeEachCallback, AfterEachCallback { override fun beforeEach(context: ExtensionContext?) { val testDispatcher = StandardTestDispatcher() privateTestDispatcher = testDispatcher - DatabaseInitialization.dispatcher = testDispatcher Dispatchers.setMain(testDispatcher) } diff --git a/codekata/robolectric.instructionset.md b/codekata/robolectric.instructionset.md index 39a60ac..fa97c04 100644 --- a/codekata/robolectric.instructionset.md +++ b/codekata/robolectric.instructionset.md @@ -186,7 +186,7 @@ class CodeKataFavouriteContentLocalStorage: KoinTest - we add a testDispatcher to Room - we switch to runTest(testDispatcher) -Since Room has their own exercutors, that could make our tests flaky, since it might get out of sync. Luckily we can switch out these executors, so we do that to make sure our tests run just as we would like them to. +Since Room has their own executors, that could make our tests flaky, since it might get out of sync. Luckily we can switch out these executors, so we do that to make sure our tests run just as we would like them to. ``` private val sut by inject() @@ -195,7 +195,7 @@ private lateinit var testDispatcher: TestDispatcher @Before fun setUp() { testDispatcher = StandardTestDispatcher() - DatabaseInitialization.dispatcher = testDispatcher + TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatcher) } @After @@ -204,15 +204,16 @@ fun tearDown() { } @Test -fun atTheStartOurDatabaseIsEmpty()= runTest(testDispatcher) { +fun atTheStartOurDatabaseIsEmpty() = runTest(testDispatcher) { sut.observeFavourites().first() } ``` -The line `DatabaseInitialization.dispatcher = testDispatcher` may look a bit mysterious, but all we do with it is to overwrite our original DatabaseInitialization in tests, and use the given Dispatcher as an executor for Room setup. +The line `TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatcher)` may look a bit mysterious, but all we do with it is overwrite the koin module to use our an InMemoryDatabase in our tests with the Dispatcher used as executor. -> DatabaseInitialization is overwritten in the Test module, by declaring the same class in the same package with the same methods. This is an easy way to switch out an implementation. -> This might not look the cleanest, so an alternative way is to switch out the koin-module of how to create the database. For this we could use loadKoinModules. In other dependency injection / service locator frameworks this should also be possible. +> Above min API 24 +> DatabaseInitialization could be overwritten in Test module, by declaring the same class in the same package with the same methods. This is an easy way to switch out an implementation. +> That might not look the cleanest, so an the presented way of switch out the koin-module creating the database is preferred. In other dependency injection / service locator frameworks this should also be possible. ### 1. `atTheStartOurDatabaseIsEmpty` @@ -518,7 +519,7 @@ fun tearDown() { ``` > Idling Resources comes from Espresso. The idea is that anytime we want to interact with the UI via Espresso, it will await any Idling Resource beforehand. This is handy, since our Network component, (OkHttp) uses it's own thread pool, and we would like to have a way to await the responses. -> Disposable is just a syntetic-sugar way to remove the OkHttpIdling resource from Espresso when we no longer need it. +> Disposable is just a synthetic-sugar way to remove the OkHttpIdling resource from Espresso when we no longer need it. > Idling Resources also makes it easy for us, to coordinate coroutines with our network responses, since we can await the IdlingResource and advance the Coroutines afterwards. ##### Coroutine Test Setup @@ -531,7 +532,7 @@ fun setup() { val dispatcher = StandardTestDispatcher() Dispatchers.setMain(dispatcher) testDispatcher = dispatcher - DatabaseInitialization.dispatcher = dispatcher + TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatcher) } @After diff --git a/codekata/sharedtests.instructionset.md b/codekata/sharedtests.instructionset.md index b4e8f87..9af368a 100644 --- a/codekata/sharedtests.instructionset.md +++ b/codekata/sharedtests.instructionset.md @@ -129,10 +129,10 @@ So that's because something went wrong in our first test. I am describing these Now, here is a new difference between Robolectric and AndroidTest. In Robolectric, before every test, the Application class is initialized, however in AndroidTests, the Application class is only initialized once. This is great if you want to have End-to-End tests that follow each other, but since now we only want to tests some small subsection of the functionality, we have to restart Koin before every tests if it isn't yet started so our tests don't use the same instances. -We will check if koin is initalized, if it isn't then we simply initalize it: +We will check if koin is initialized, if it isn't then we simply initialize it: ```kotlin ... -DatabaseInitialization.dispatcher = dispatcher +TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher) if (GlobalContext.getOrNull() == null) { val application = ApplicationProvider.getApplicationContext() val baseUrl = BaseUrl(BuildConfig.BASE_URL) @@ -237,7 +237,7 @@ Let's create a TestRule for that setup next. val dispatcher = StandardTestDispatcher() Dispatchers.setMain(dispatcher) testDispatcher = dispatcher -DatabaseInitialization.dispatcher = dispatcher +TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher) // after Dispatchers.resetMain() @@ -253,7 +253,7 @@ override fun apply(base: Statement, description: Description): Statement = objec try { val dispatcher = StandardTestDispatcher() Dispatchers.setMain(dispatcher) - DatabaseInitialization.dispatcher = dispatcher + TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher) _testDispatcher = dispatcher base.evaluate() } finally {