Fix swapping out Database in tests

Previously overwrote the object itself for a quick swap of DatabaseInitialization, but that only works over API 24.
So now we will use loadKoinModules instead, which resolves the issue on ani API level
This commit is contained in:
Gergely Hegedus 2022-04-13 19:13:34 +03:00
parent 78a877b0c9
commit 1d2ca90203
11 changed files with 57 additions and 47 deletions

View file

@ -12,12 +12,12 @@ 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.network.testutil.NetworkTestConfigurationHelper 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.CompositeDisposable
import org.fnives.test.showcase.testutils.idling.Disposable import org.fnives.test.showcase.testutils.idling.Disposable
import org.fnives.test.showcase.testutils.idling.IdlingResourceDisposable import org.fnives.test.showcase.testutils.idling.IdlingResourceDisposable
import org.fnives.test.showcase.testutils.idling.OkHttp3IdlingResource import org.fnives.test.showcase.testutils.idling.OkHttp3IdlingResource
import org.fnives.test.showcase.testutils.idling.loopMainThreadFor 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.fnives.test.showcase.ui.splash.SplashActivity
import org.hamcrest.Description import org.hamcrest.Description
import org.hamcrest.Matcher import org.hamcrest.Matcher
@ -45,7 +45,7 @@ class LoginLogoutEndToEndTest {
@Before @Before
fun before() { fun before() {
/** Needed to add the dispatcher to the Database */ /** 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.*/ /** Needed to register the Okhttp as Idling resource, so Espresso actually waits for the response.*/
val idlingResources = NetworkTestConfigurationHelper.getOkHttpClients() val idlingResources = NetworkTestConfigurationHelper.getOkHttpClients()

View file

@ -12,7 +12,7 @@ import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId 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.After
import org.junit.Assert import org.junit.Assert
import org.junit.Before import org.junit.Before
@ -36,7 +36,7 @@ internal class FavouriteContentLocalStorageImplInstrumentedTest(
@Before @Before
fun setUp() { fun setUp() {
testDispatcher = StandardTestDispatcher() testDispatcher = StandardTestDispatcher()
DatabaseInitialization.dispatcher = testDispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatcher)
sut = favouriteContentLocalStorageFactory() sut = favouriteContentLocalStorageFactory()
} }

View file

@ -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.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.storage.database.DatabaseInitialization
import org.fnives.test.showcase.testutils.idling.CompositeDisposable import org.fnives.test.showcase.testutils.idling.CompositeDisposable
import org.fnives.test.showcase.testutils.idling.Disposable import org.fnives.test.showcase.testutils.idling.Disposable
import org.fnives.test.showcase.testutils.idling.IdlingResourceDisposable 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.MainDispatcherTestRule.Companion.advanceUntilIdleWithIdlingResources
import org.fnives.test.showcase.testutils.idling.OkHttp3IdlingResource import org.fnives.test.showcase.testutils.idling.OkHttp3IdlingResource
import org.fnives.test.showcase.testutils.safeClose 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.fnives.test.showcase.ui.auth.AuthActivity
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -45,7 +45,7 @@ class RobolectricAuthActivityInstrumentedTest : KoinTest {
val dispatcher = StandardTestDispatcher() val dispatcher = StandardTestDispatcher()
Dispatchers.setMain(dispatcher) Dispatchers.setMain(dispatcher)
testDispatcher = dispatcher testDispatcher = dispatcher
DatabaseInitialization.dispatcher = dispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher)
mockServerScenarioSetup = NetworkTestConfigurationHelper.startWithHTTPSMockWebServer() mockServerScenarioSetup = NetworkTestConfigurationHelper.startWithHTTPSMockWebServer()

View file

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

View file

@ -3,8 +3,8 @@ 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.storage.database.DatabaseInitialization
import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent
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
@ -20,7 +20,7 @@ class DispatcherTestRule : TestRule {
override fun evaluate() { override fun evaluate() {
val dispatcher = StandardTestDispatcher() val dispatcher = StandardTestDispatcher()
testDispatcher = dispatcher testDispatcher = dispatcher
DatabaseInitialization.dispatcher = dispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher)
base.evaluate() base.evaluate()
} }
} }

View file

@ -6,8 +6,8 @@ import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher 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.storage.database.DatabaseInitialization
import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent
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
@ -24,7 +24,7 @@ class MainDispatcherTestRule : TestRule {
val dispatcher = StandardTestDispatcher() val dispatcher = StandardTestDispatcher()
Dispatchers.setMain(dispatcher) Dispatchers.setMain(dispatcher)
testDispatcher = dispatcher testDispatcher = dispatcher
DatabaseInitialization.dispatcher = dispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher)
try { try {
base.evaluate() base.evaluate()
} finally { } finally {

View file

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

View file

@ -8,6 +8,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
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.storage.database.DatabaseInitialization import org.fnives.test.showcase.storage.database.DatabaseInitialization
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
@ -28,7 +29,7 @@ class PlainMainDispatcherRule(private val useStandard: Boolean = true) : TestRul
try { try {
val dispatcher = if (useStandard) StandardTestDispatcher() else UnconfinedTestDispatcher() val dispatcher = if (useStandard) StandardTestDispatcher() else UnconfinedTestDispatcher()
Dispatchers.setMain(dispatcher) Dispatchers.setMain(dispatcher)
DatabaseInitialization.dispatcher = dispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher)
_testDispatcher = dispatcher _testDispatcher = dispatcher
base.evaluate() base.evaluate()
} finally { } finally {

View file

@ -6,14 +6,13 @@ import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher 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.storage.database.DatabaseInitialization
import org.fnives.test.showcase.testutils.TestMainDispatcher.Companion.testDispatcher import org.fnives.test.showcase.testutils.TestMainDispatcher.Companion.testDispatcher
import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext 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. * One can access the test dispatcher via [testDispatcher] static getter.
*/ */
@ -23,7 +22,6 @@ class TestMainDispatcher : BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) { override fun beforeEach(context: ExtensionContext?) {
val testDispatcher = StandardTestDispatcher() val testDispatcher = StandardTestDispatcher()
privateTestDispatcher = testDispatcher privateTestDispatcher = testDispatcher
DatabaseInitialization.dispatcher = testDispatcher
Dispatchers.setMain(testDispatcher) Dispatchers.setMain(testDispatcher)
} }

View file

@ -186,7 +186,7 @@ class CodeKataFavouriteContentLocalStorage: KoinTest
- we add a testDispatcher to Room - we add a testDispatcher to Room
- we switch to runTest(testDispatcher) - 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<FavouriteContentLocalStorage>() private val sut by inject<FavouriteContentLocalStorage>()
@ -195,7 +195,7 @@ private lateinit var testDispatcher: TestDispatcher
@Before @Before
fun setUp() { fun setUp() {
testDispatcher = StandardTestDispatcher() testDispatcher = StandardTestDispatcher()
DatabaseInitialization.dispatcher = testDispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatcher)
} }
@After @After
@ -204,15 +204,16 @@ fun tearDown() {
} }
@Test @Test
fun atTheStartOurDatabaseIsEmpty()= runTest(testDispatcher) { fun atTheStartOurDatabaseIsEmpty() = runTest(testDispatcher) {
sut.observeFavourites().first() 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. > Above min API 24
> 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. > 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` ### 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. > 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. > 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 ##### Coroutine Test Setup
@ -531,7 +532,7 @@ fun setup() {
val dispatcher = StandardTestDispatcher() val dispatcher = StandardTestDispatcher()
Dispatchers.setMain(dispatcher) Dispatchers.setMain(dispatcher)
testDispatcher = dispatcher testDispatcher = dispatcher
DatabaseInitialization.dispatcher = dispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatcher)
} }
@After @After

View file

@ -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. 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. 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 ```kotlin
... ...
DatabaseInitialization.dispatcher = dispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher)
if (GlobalContext.getOrNull() == null) { if (GlobalContext.getOrNull() == null) {
val application = ApplicationProvider.getApplicationContext<TestShowcaseApplication>() val application = ApplicationProvider.getApplicationContext<TestShowcaseApplication>()
val baseUrl = BaseUrl(BuildConfig.BASE_URL) val baseUrl = BaseUrl(BuildConfig.BASE_URL)
@ -237,7 +237,7 @@ Let's create a TestRule for that setup next.
val dispatcher = StandardTestDispatcher() val dispatcher = StandardTestDispatcher()
Dispatchers.setMain(dispatcher) Dispatchers.setMain(dispatcher)
testDispatcher = dispatcher testDispatcher = dispatcher
DatabaseInitialization.dispatcher = dispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher)
// after // after
Dispatchers.resetMain() Dispatchers.resetMain()
@ -253,7 +253,7 @@ override fun apply(base: Statement, description: Description): Statement = objec
try { try {
val dispatcher = StandardTestDispatcher() val dispatcher = StandardTestDispatcher()
Dispatchers.setMain(dispatcher) Dispatchers.setMain(dispatcher)
DatabaseInitialization.dispatcher = dispatcher TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher)
_testDispatcher = dispatcher _testDispatcher = dispatcher
base.evaluate() base.evaluate()
} finally { } finally {