How to override dependency into real kodein app dependency graph? - android

I'm trying to make some tests and i need to replace the real dependency by a fake one by overriding it on KODEIN but it's not working and i don't know what i can do anymore.
Here is my dependency graph (I'm omitting others dependencies):
class Injector(private val context: Context) {
val dependencies = Kodein.lazy {
.
.
bind<RetrieveContacts>() with provider {
Log.i("RetrieveContacts","REAL")
RetrieveContactsInMemory()
}
.
.
}
}
Here is my application class:
class AppApplication : Application(), KodeinAware {
override val kodein by Injector(this).dependencies
}
Here is what i'm doing to override the dependency:
#RunWith(value = AndroidJUnit4::class)
class HomeActivityEmptyStateTest {
#get:Rule
var mActivityTestRule = ActivityTestRule<HomeActivity>(HomeActivity::class.java)
#Before
fun setup() {
val appApplication = InstrumentationRegistry.getInstrumentation().targetContext
val kodein by Kodein.lazy {
extend(appApplication.appKodein())
bind<RetrieveContacts>(overrides = true) with provider {
Log.i("RetrieveContacts","FAKE")
RetrieveEmptyContacts()
}
}
}
#Test
fun testWhenHasNoContent() {
...
}
}
I'm still seeing "RetrieveContacts REAL" instead of "RetrieveContacts FAKE" in console log

It seems like you forgot to allow overrides when extending in your test.
extend(appApplication.appKodein(), allowOverride = true)

This does not work since the parents definition will not be overridden. more info: https://kodein.org/Kodein-DI/?6.1/core#_overridden_access_from_parent
This is because Bar is bound to a singleton, the first access would
define the container used (parent or child). If the singleton were
initialized by child, then a subsequent access from parent would yeild
a Bar with a reference to a Foo2, which is not supposed to exist in
parent.

Related

Android Instrumented tests with KodeIn

We have an Android app that is using compose for the view layer and we are using Kodein for all of our dependency injections.
I have a BaseApplication class which is DIAware:
class BaseApplication : Application(), DIAware {
override val di: DI = DI.lazy {
import(modules) // modules defined in respective packages
}
}
I also have a MainActivity and a nav graph to manage navigation between the various composables.
Problem:
How can I properly override these modules in my instrumented tests for MainActivity?
#RunWith(AndroidJUnit4::class)
class MainActivityTest {
#get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
val moduleOverrides = DI.Module(allowSilentOverride = true) {
// add bindings for fakes w/ allowOverride = true
}
#Before
fun setup() {
val application =
ApplicationProvider.getApplicationContext() as BaseApplication
// how can I override the BaseApplication modules for my test?
}
}
I cant seem to find any clear language on the matter and feel like I am missing something very obvious. Any help would be very much appreciated.
There are several ways to achieve that. The general approach is to override the actual modules like
val someParrentKodeinModule...
val mockModule = Kodein {
extend(someParrentKodeinModule, allowOverride = true)
bind<Foo>(overrides = true) with provider { Foo2() }
}
or
val kodein = Kodein {
/* ... */
import(testsModule, allowOverride = true)
}
where testsModule is some module that already defines all the needed mock components that will be overridden in the main one.
Your approach is also good. The key point is to replace your DI with the needed one - this can be done making the DI in your app - var instead of val and assigning new value to it. But you will have to drop DIAware
class BaseApplication : Application() {
var di: DI = DI.lazy {
import(modules) // modules defined in respective packages
}
}
#Before
fun setup() {
val application =
ApplicationProvider.getApplicationContext() as BaseApplication
application.di = moduleOverrides
}
Something like that.
And generally using single DI for the app inside the App class is not recommended. Use specialized modules for each component of the app you want to test

Android Test Koin NoBeanDefFoundException

I'm trying to do some Android Tests with Koin and so far, it is not a success.
I want to test a basic Activity with a ViewModel, injected by Koin.
I already read posts like NoBeanDefFoundException with Mock ViewModel, testing with Koin, Espresso but so far I still have the error.
Here is the code relative to the tests configuration
A specific app that start with no module.
class MyTestApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin { emptyList<Module>() }
}
}
A specific runner that uses the test app
class OccazioTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, MyTestApplication::class.java.name, context)
}
}
That is defined in my app build.gradle to be used as runner
android {
defaultConfig {
testInstrumentationRunner "fr.dsquad.occazio.occazio.OccazioTestRunner"
}
}
And now the code I want to test
In my MyActivity
class MyActivity : AppCompatActivity(R.layout.activity_my) {
private val myViewModel by viewModel<MyViewModel>()
// Some code
}
And the viewmodel
class MyViewModel(private val useCase: MyUseCase): ViewModel() {
// Some code
}
And finally, the test itself (in androidTest)
#LargeTest
class MyActivityTest : KoinTest {
private lateinit var mockUseCase: MyUseCase
#JvmField
#Rule
val activityRule = activityScenarioRule<MyActivity>()
#Before
fun setup() {
mockUseCase = mock(MyUseCase::class.java)
startKoin {
modules(module { viewModel { MyViewModel(mockUseCase) } })
}
// I've also tried this
loadKoinModules(
module { viewModel { MyViewModel(mockUseCase) } }
)
}
#After
fun cleanUp() {
stopKoin()
}
#Test
fun someTest() = runBlocking {
// Mock the usecase response
`when`(mockUseCase.doSomething()).thenReturn("taratata")
// Start the scenario
val scenario = activityRule.scenario
// Verify we call the getUserId
// Activity is supposed to call the view model that will call the method doSomethingAdterThat.
verify(mockUseCase, times(1)).doSomethingAfterThat()
return#runBlocking
}
}
And so far, everytime I run this code I have this error
org.koin.core.error.NoBeanDefFoundException:
No definition found for 'mypackage.MyViewModel' has been found. Check your module definitions.
What is interesting is that, when
I change the rule activityScenarioRule by the old deprecated ActivityTestRule(SplashScreenActivity::class.java, true, false)
I change val scenario = activityRule.scenario by val scenario = activityRule.launchActivity(null)
I use loadKoinModules and not startKoin in setUp
Two things happen
When my test is started alone (via Android Studio): it passes.
When my test is started with other tests (by the class or with connectedAndroidTest), only one of them passes and old the others are KO.
So I have two questions in fact here.
How can I make this test work with activityScenarioRule ?
How can I make them "all" work (and not start them one by one to make them work) ?
Ok, don't ask me how it works but I figured it out.
First of all, as I needed config I followed this https://medium.com/stepstone-tech/better-tests-with-androidxs-activityscenario-in-kotlin-part-1-6a6376b713ea .
I've done 3 things
First, I needed to configure koin before startup, to do that, I needed to use ActivityScenario.launch() with an intent that I defined earlier
private val intent = Intent(ApplicationProvider.getApplicationContext(), MyActivity::class.java)
var activityRule : ActivityScenario<MyActivity>? = null
// And then I can start my activity calling
activityRule = ActivityScenario.launch(intent)
Then "KoinApp was not started"... I just replaced the loadKoinModules block with the startKoin one in setUp
startKoin { modules(module { viewModel { MyViewModel(mockUseCase) } }) }
Finally, it worked for 1 test, but the others were failing because "KoinAppAlreadyStartedException" like the stopKoin() was not called. So I found out that I should extend AutoCloseKoinTest instead of KoinTest.. But no success.
In the end, I've put a stopKoin() before the startKoin and now, everything works like a charm.
Here is my complete code that works
#LargeTest
class MyActivityTest : KoinTest() {
private val intent = Intent(ApplicationProvider.getApplicationContext(), MyActivity::class.java)
var activityRule : ActivityScenario<MyActivity>? = null
private lateinit var mockUseCase: MyUseCase
#Before
fun setup() {
mockUseCase = mock(MyUseCase::class.java)
stopKoin()
startKoin {
modules(module { viewModel { MyViewModel(mockUseCase) } })
}
}
#After
fun cleanUp() {
activityRule?.close()
}
#Test
fun someTest() = runBlocking {
// Mock the usecase response
`when`(mockUseCase.doSomething()).thenReturn("taratata")
// Start the rule
val activityRule = ActivityScenario.launch(intent)
// Verify we call the getUserId
// Activity is supposed to call the view model that will call the method doSomethingAdterThat.
verify(mockUseCase, times(1)).doSomethingAfterThat()
return#runBlocking
}
}
Ho, I've also added this code to my two Applications
override fun onTerminate() {
super.onTerminate()
stopKoin()
}
Just to be sure !

Dagger2 Provider in koin

Is there any alternative to javax.inject.Provider in koin?
To react to actions, I am injecting Commands to my activity.
Command is a single-run object, for example WriteToFile.
In dagger I could make it like this:
class MainPresenter : Presenter() {
#Inject
lateinit var writeFile: Provider<WriteFileCommand>
fun onSaveClicked() {
writeFile.get().run()
}
}
in koin, when I try to use:
class MainPresenter : Presenter() {
lateinit var writeFile: Provider<WriteFileCommand> by inject()
fun onSaveClicked() {
writeFile.get().run()
}
}
My koin module:
val appModule = module {
factory { WriteFileCommand(get(), get()) }
factory { FileProvider() }
single { DataStore() }
}
Than I got error saying:
Can't create definition for 'Factory [name='WriteFileCommand',class='com.test.WriteFileCommand']' due to error :
No compatible definition found. Check your module definition
I understand that I can call:
var command: WriteFileCommand = StandAloneContext.getKoin().koinContext.get()
command.run()
But It looks so cumbersome
There's nothing like a provider directly. If you use inject, you'll use a lazy delegate. If you use get, you'll create a new instance you declared the dependency with a factory. So get is what you need in your case. Just let your MainPresenter implement KoinComponent and you'll be able to use get directly:
class MainPresenter : Presenter(), KoinCompontent {
fun onSaveClicked() = get<WriteFileCommand>().run()
}

How to use Koin in multiple module?

There are two modules in my android project, app module and lib module.
Both these two modules need Koin for D.I., so I call startKoin in MyApplication class in app module, and IninKointContentProvider in lib module as below.
// app module
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin(this, modules1)
}
}
// lib module
class InitKoinContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
startKoin(context.applicationContext, modules2)
return true
}
}
Then app crashed and shown this message
Caused by: org.koin.error.BeanOverrideException: Try to override definition with Single [class='android.content.Context'], but override is not allowed. Use 'override' option in your definition or module.
I guess startKoin can be called only one time.
The solution I found is merging two koin modules then calling startKoin in MyApplication, but I don't like it. Lib module may be imported by other android project which doesn't use koin, in that case, I think calling startKoin in InitKoinContentProvider is better.
Any solution for this problem?? Thanks!
I found the best solution inspired by #laalto's answer, thanks!
Upgrade to koin 2.0, then use KoinApplication and customized KoinComponent to create a isolated koin context, it can let lib module using koin without any initializing call by app module, still start koin in ContentProvider. The whole code may like below.
// app module
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this#MyApplication)
modules(module{
viewModel { MainViewModel() }
})
}
}
}
class MainActivity: AppCompactActivity() {
private val viewModel: MainViewModel by viewModel()
}
// lib module
internal object MyKoinContext {
lateinit var koinApplication: KoinApplication
}
interface MyKoinComponent : KoinComponent {
override fun getKoin(): Koin {
return MyKoinContext.koinApplication.koin
}
}
class InitKoinContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
MyKoinContext.koinApplication = koinApplication {
androidContext(context.applicationContext)
modules(module{
viewModel { FooViewModel() }
})
}
return true
}
}
class FooActivity: AppCompactActivity(), MyKoinComponent {
private val viewModel: FooViewModel by viewModel()
}
Ref:
https://insert-koin.io/docs/2.0/documentation/reference/index.html#_koin_context_isolation
In your library modules, use loadKoinModules() to load the module-specific koin modules. Docs.
You need to have run startKoin() prior to that, so the init order with content providers can be a little tricky.
To init extra koin modules on other project modules and get no duplicate loading issues (e.g. pressing home, than coming back to the activity), go to your module declaration file:
val myModule = module {
single { MyRepository(get()) }
viewModel { MyViewModel(get()) }
}
private val loadKoinModules by lazy {
loadKoinModules(myModule)
}
fun inject() = loadKoinModules
Then on your view:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inject()
}
TL;DR
Use single/factory methods with override param set to true when providing your dependencies that are overriding those provided by the modules loaded before.
single<Manager>(override = true) { TestManager() }
I have faced a similar issue when I tried to override one of the dependencies for UI test purposes.
When I setup in Application.onCreate():
startKoin {
module {
single { Printer() }
}
}
and then in before method of test:
loadKoinModules(module {
single<Printer> { TestPrinter() }
})
I get a Runtime exception during the test:
org.koin.core.error.DefinitionOverrideException: Already existing definition or try to override an existing one
And the solution is to show Koin that you are intentionally overriding that dependency by using override param of single function like that:
loadKoinModules(module {
single<Printer>(override = true) { TestPrinter() }
})
By design startKoin is meant to be called from the Application class. You can provide a parameter in the lib whether to call startKoin or not. But I doubt that including such things as Koin in libs is a good practice. What if an application already includes Koin, but of different version?
This way worked perfect for me:
#KoinExperimentalAPI
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
initKoin()
}
private fun initKoin() {
startKoin {
androidLogger()
androidContext(this#MainApplication)
}
loadKoinModules(
myFeatureModule
)
}
}
And define you module in your feature as usual:
val myFeatureModule = module {
factory<...> { ...() }
single { ...() }
viewModel { ...(get() }
}

How do I inject a class that uses androidContext in an instrumented test with Koin?

One of my classes has a dependency of type Context. Before adding Koin to my project, I initialized this with a hard dependency on my Application class:
class ProfileRepository(
private var _context: Context? = null,
private var _profileRestService: IProfileRestService? = null
) : IProfileRepository {
init {
if (_context == null) {
_context = MyApplication.getInstance().applicationContext
}
}
Now, I want to use Koin to inject this dependency. This is how I've defined the module:
object AppModule {
#JvmField
val appModule = module {
single<IProfileRestService> { ProfileRestService() }
single<IProfileRepository> { ProfileRepository(androidContext(), get()) }
}
}
I'm starting Koin in the onCreate method of my Application class (which is written in Java):
startKoin(singletonList(AppModule.appModule));
I want to test this class with an instrumented test and not a unit test because I want to use the real context and not a mock. This is my test:
#RunWith(AndroidJUnit4::class)
class MyTest : KoinTest {
private val _profileRepository by inject<IProfileRepository>()
#Test
fun testSomething() {
assertNotNull(_profileRepository)
}
The test is failing with an exception:
org.koin.error.BeanInstanceCreationException: Can't create definition for 'Single [name='IProfileRepository',class='com.my.app.data.profile.IProfileRepository']' due to error :
No compatible definition found. Check your module definition
I can get it to work with a unit test if I mock the context like so:
class MyTest : KoinTest {
private val _profileRepository by inject<IProfileRepository>()
#Before
fun before() {
startKoin(listOf(AppModule.appModule)) with mock(Context::class.java)
}
#After
fun after() {
stopKoin()
}
#Test
fun testSomething() {
assertNotNull(_profileRepository)
}
How can I make it work as an instrumented test with a real context?
In place of (in Application):
startKoin(applicationContext, modules)
Use a mocked Context:
startKoin(modules) with (mock(Context::class.java))
From https://insert-koin.io/docs/1.0/documentation/koin-android/index.html#_starting_koin_with_android_context_from_elsewhere
Apparently there's no way to start Koin from a Java class and inject the application context. What that means is if one of your classes needs to get the context from the container, you must use org.koin.android.ext.android.startKoin instead of org.koin.java.standalone.KoinJavaStarter.startKoin.
Since my Application class is still written in Java, I created an object called KoinHelper with one method:
#JvmStatic
fun start(application: Application) {
application.startKoin(application, listOf(AppModule.appModule))
}
Then I called this from the onCreate method of my Application class:
KoinHelper.start(this);
Now, the instrumented test I posted in my original answer runs just fine.
Please see this issue on GitHub for more info.
Please check this section in the documentation. It says
if you need to start Koin from another Android class, you can use the
startKoin() function and provide your Android Context instance with
just like:
startKoin(androidContext, myAppModules)
So in your instrumentation test, you can pass a context while starting the Koin.
#Before
fun before() {
startKoin(InstrumentationRegistry.getContext(), listOf(AppModule.appModule))
}
Or if you want an application level context
#Before
fun before() {
startKoin(InstrumentationRegistry.getTargetContext(), listOf(AppModule.appModule))
}
The referenced documentation is for Version 1.0.1
In terms of getting the Application context in the instrumented test, you can use androidx.test.core.app.ApplicationProvider or InstrumentationRegistry.targetContext.applicationContext.
#Before
fun setUp() {
stopKoin()
loadKoinModules(testModule) with ApplicationProvider.getApplicationContext<Application>()
}
...where testModule uses androidApplication() to retrieve the Application context:
val testModule = module {
single {
ToDoDatabase.newInstance(
androidApplication(),
memoryOnly = true
)
}
single { ToDoRepository(get()) }
}
Note that my stopKoin() call is there because I was having difficulty overriding an existing module created by startKoin() in my custom Application subclass. ¯\_(ツ)_/¯
#Before
fun setUp() {
stopKoin()
startKoin {
androidContext(app) // for example ApplicationProvider.getApplicationContext<TestApplication>()
modules(module1, module2)
}
}

Categories

Resources