How to inject test overrides into the default dependency graph? - android

I would like to inject mocked overrides into my Android instrumentation tests using Kodein. I don't know which is the optimal approach to do this. Here's what I have in mind:
My app uses a KodeinAware application class. The served Kodein instance holds all dependencies required by my app.
In my tests I would like to inject mocked overrides for specific dependencies to test behavior of the app in various situations.
Overrides should be different for each test, and should be injected before/while the test runs.
Is the configurable Kodein extension sensible in this situation, or is there a simpler, better suited approach (and if so, which)?

If your test is given a Kodein instance (meaning that it can use a different Kodein object than the one held by your Application), then the recommended approach is to create a new Kodein object that extends the one of the app and overrides all necessary bindings.
val testKodein = Kodein {
extend(appKodein())
bind<MyManager>(overrides = true) with singleton { mock<MyManager>() }
}
The configurable Kodein option is recommended only if you're using a static "one true Kodein". Using it prevents the possibility to run you're tests in parallel (because they all access the same Kodein instance), and forces you to clear the ConfigurableKodein between each tests and re-declare every time different overrides.

I am now using the ConfigurableKodein inside my custom App class.
class App : Application(), KodeinAware {
override val kodein = ConfigurableKodein()
override fun onCreate() {
super.onCreate()
// A function is used to create a Kodein module with all app deps.
kodein.addImport(appDependencies(this))
}
}
// Helper for accessing the App from any context.
fun Context.asApp() = this.applicationContext as App
Inside my AppTestRunner class, I declare the configuration to be mutable. That way I can reset it's configuration between each and every test.
class AppTestRunner : AndroidJUnitRunner() {
override fun callApplicationOnCreate(app: Application) {
app.asApp().kodein.mutable = true
super.callApplicationOnCreate(app)
}
}
I have created a JUnit rule that reset the dependency graph before every test.
class ResetKodeinRule : ExternalResource() {
override fun before() {
val app = InstrumentationRegistry.getInstrumentation().targetContext.asApp()
app.kodein.clear()
app.kodein.addImport(appDependencies(app))
}
}
In my tests I can now retrieve the App.kodein instance and inject mocks that override dependencies of the original graph. The only thing that needs to be guaranteed is that the tested activity is launched after configuring mocks, or behavior is not predictable.

Related

Koin Context Isolation: setup correctly and avoid random NoBeanDefFoundException

I have an Android library module that I package and publish. It's a dependency in different apps, some that use Koin and some that don't, therefore I want to use Koin's context isolation.
Having followed the docs, I added a koin component and context as follows:
internal object LocalKoinContext {
lateinit var koinApplication: KoinApplication
}
// Custom KoinComponent using the local instance & not the Global context
interface CustomKoinComponent : KoinComponent {
// override the used Koin instance to use the local koin application instance (LocalKoinContext.koinApplication)
override fun getKoin(): Koin = LocalKoinContext.koinApplication.koin
}
and add create the koinApplication like this:
// Local Koin application instance
LocalKoinContext.koinApplication = koinApplication {
// use AndroidLogger as Koin Logger - default Level.INFO
androidLogger(Level.ERROR)
// use the Android context given there
androidContext(applicationContext)
// load properties from assets/koin.properties file
androidFileProperties()
// declare used modules
modules(theModule)
}
I have an activity that uses a view model that lives within the local Koin component and I used to instantiate the koinApplication within the onCreate of the that activity:
class FirstActivity : AppCompatActivity(),
CustomKoinComponent {
private val sharedViewModel by viewModel<MySharedViewModel>()
...
override fun onCreate(savedInstanceState: Bundle?) {
...
// Local Koin application instance
LocalKoinContext.koinApplication = koinApplication {
// use AndroidLogger as Koin Logger - default Level.INFO
androidLogger(Level.ERROR)
// use the Android context given there
androidContext(applicationContext)
// load properties from assets/koin.properties file
androidFileProperties()
// declare used modules
modules(theModule)
}
}
The issue is that I get random NoBeanDefFoundException
Caused by org.koin.core.error.NoBeanDefFoundException: No definition found for class:'my.package.name.MySharedViewModel'. Check your definitions!
at org.koin.core.scope.Scope.throwDefinitionNotFound(Scope.java:264)
at org.koin.core.scope.Scope.resolveInstance(Scope.java:233)
at org.koin.core.scope.Scope.get(Scope.java:204)
at org.koin.androidx.viewmodel.factory.DefaultViewModelFactory.create(DefaultViewModelFactory.java:11)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)
at org.koin.androidx.viewmodel.ViewModelResolverKt.get(ViewModelResolverKt.java:23)
at org.koin.androidx.viewmodel.ViewModelResolverKt.resolveInstance(ViewModelResolverKt.java:12)
at org.koin.androidx.viewmodel.scope.ScopeExtKt.getViewModel(ScopeExtKt.java:86)
at org.koin.androidx.viewmodel.scope.ScopeExtKt.getViewModel(ScopeExtKt.java:72)
at org.koin.androidx.viewmodel.ext.android.ViewModelStoreOwnerExtKt.getViewModel(ViewModelStoreOwnerExtKt.java:68)
at org.koin.androidx.viewmodel.ext.android.ViewModelStoreOwnerExtKt.getViewModel$default(ViewModelStoreOwnerExtKt.java:58)
at my.package.name.FirstActivity$special$$inlined$viewModel$default$1.invoke(FirstActivity.java:75)
I suspect it's because sometimes the koinApplication doesn't have time to set itself up but as it's rare I haven't been able to reproduce the issue.
In the answer to this stack question, the dev creates a ContentProvider to instantiate the koinApplication. I've tried it out and so far so good (although, I will only know in months from now), but is this truly the way to go? Seems to be an overkill to have to create a content provider just for that.
Can anyone shed some light on where we are supposed to instantiate the local KoinApplication?
Can anyone shed some light on where we are supposed to instantiate the local KoinApplication?
You should do this inside your Application level class, for example this is what I have on my Application class onCreate
startKoin {
androidLogger(Level.ERROR)
androidContext(this#VeroApp)
modules(AppModules.modules) <-- list of defined koin modules
}
I'm not so sure about the usage of local KoinApplication interface. I know that AppCompatActivity for example is out of the box supported and you can just do a without extending anything
private val sharedViewModel by viewModel<MySharedViewModel>()

Inject into arbitrary Logic class that is not instanciated by Hilt

I'm currently migrating an app from anko-sqlite to room as anko has been discontinued a long time ago. In the process of doing so I introduced a repository layer and thought I would try to introduce dependency injection to get references to the repository instances in all my classes.
Prior to this I used a singleton instance that I just shoved into my application classes companion object and accessed that from everywhere.
I managed to inject my repositories into Fragments, Workmanager and Viewmodels. But I do now struggle a bit to understand how they forsaw we should handle this with arbitrary logic classes.
For instance my workmanager worker calls a class that instantiates a list of "jobs" and those need access to the repository. The worker itself actually bearly even does need access to the repositories as all the work is abstracted away from it.
How can I make something like this work
class Job(val someExtraArgINeed: Int) {
#Inject
lateinit var someRepository: SomeRepository // <= this should be injected when the job is instanciated with the constructor that already exists
}
For Activities and so forth we have special annotations #AndroidEntryPoint that makes this work. However, the documentation makes it unclear as to how we should get our instances from any other class that isn't an Android class. I understand that constructor injection is the recommended thing to use. But I do not think it would work here for me without an even more massive refactor required than this already would be.
If I understand your question correctly you can use hilt entry points like this
class CustomClass(applicationContext: Context) {
#EntryPoint
#InstallIn([/*hilt component that provide SomeRepository*/ApplicationComponent]::class)
interface SomeRepositoryEntryPoint {
fun provideSomeRepository(): SomeRepository
}
private val someRepository by lazy {
EntryPointAccessors.fromApplication(applicationContext, SomeRepositoryEntryPoint::class.java).provideSomeRepository()
}
fun doSomething() {
someRepository.doSomething()
}
}

WorkManager : How to set-up different WorkManager Configurations in same App

I'm working on a multi module project (Gradle module). I'm using WorkManager in my module. I'm also making use of Dagger for dependency injection.
Now I have to use dagger to inject dependencies to my WorkManager. I'm quite familiar with Dagger 2 setup with WorkManager. But the problem I'm facing is, I have to use
worker factory to make it compatible with dagger. So that I can inject dependencies with the help of Dagger Multi bindings. But currently the WorkManager configuration in the main module (Main app gradle module) is
public Configuration getWorkManagerConfiguration() {
return new Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.build();
}
Which doesn't use a custom factory. And already several other modules (gradle modules for other features) are using WorkManger without factory. Now If I change this configuration and add a factory, it might break the work manager setup in several other place. Can I make use of a factory only for the WorkManager classes in my module (or only some work manager classes should be initialized via factory, others use default configuration). Is it possible to do? Hope my problem is clear.
You can use a DelegatingWorkerFactory and add you're custom WorkerFactory to it.
Your custom WorkerFactory will need to check if the classname passed to the factory is the one it want to handle, if not, just return null and the DelegatingWorkerFactory will revert to the default worker factory using reflection.
Keep in mind that you need to add your custom WorkerFactory each time you initialize WorkManager. If you don't do that and WorkManager tries to fullfill a WorkRequest for your Worker (that is normally handled by the custom WorkerFactory) it will fallback to the default WorkerFactory and fail (probably with a class not found exception).
We are using the DelegatingWorkerFactory in IOsched, the scheduling app used for I/O and the Android Developer Summit.
The code of your custom WorkerFactory is going to be something like:
class ConferenceDataWorkerFactory(
private val refreshEventDataUseCase: RefreshConferenceDataUseCase
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
return when (workerClassName) {
ConferenceDataWorker::class.java.name ->
ConferenceDataWorker(appContext, workerParameters, refreshEventDataUseCase)
else ->
// Return null, so that the base class can delegate to the default WorkerFactory.
null
}
}
}

Koin Android Test

I have a problem with Koin & "androidTest".
Because androidTest starts the Application i don't need to start Koin by myself in the test.
Now i need to inject a mock service. The problem is, that i inject inside of a method with get() inside of a singleton class and this is not working via constructor injection because the injected object can have different implementations.
My idea was to declare what i need this way:
declare {
factory<Webservice>(override = true) { mockWebservice }
}
But this will be applied on all tests. That's why an other test, which checks if the correct class was injected failed.
I also tried to use stopKoin(), startKoin(listOf(appModule)) in the #After method, but with this the dependency injection doesn't work anymore in later tests.
Is there a way to declare the mock only for one test?
Here is how I do it in my Android Tests:
In a parent test class, I use these methods for setup and teardown:
#Before fun startKoinForTest() {
if (GlobalContext.getOrNull() == null) {
startKoin {
androidLogger()
androidContext(application)
modules(appComponent)
}
}
}
#After fun stopKoinAfterTest() = stopKoin()
My appcomponent contains all modules needed for the dependency tree.
Then, when I want to mock a dependency for a specific test, I use something like this:
declareMock<TripApi> { given(this.fetch(any())).willReturn(TestData.usaTrip) }
You will need to add a new mock declaration for each test if you wish to swap a dependency with a mock.
To declare mock only for one test you can use loadKoinModules()
You can’t call the startKoin() function more than once. But you can use directly the loadKoinModules() functions.
So this way your definition will override default one
loadKoinModules(module {
factory<Webservice>(override = true) { mockWebservice }
})
Also, don't forget to implement KoinTest interface in you test class

How to mock after setup dagger-android 2.15 when writing espresso tests?

If we just use plain dagger 2. In the application class, we will have a property which holds the AppComponent. Then we can swap it during espresso tests.
But when I setup my project using dagger-android 2.15. Things becomes more implicit if adopt too much Dagger magic. The code is more clean, but makes testing a little bit hard.
This is the application class:
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent
.builder()
.create(this)
.build()
}
}
This is the HomeActivity
class HomeActivity : DaggerAppCompatActivity() {
#Inject
lateinit var userPreference: UserPreference
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
if (!this.userPreference.memberRegistered) {
goToActivity(EntryActivity::class.java)
}
}
}
Take this code for example. How to mock that injected userPreference.memberRegistered Which could be a HTTP call underneath?
For those who is interested in this, I got a blog with step by step detail for this:
Basically, the idea is:
You still generate instances for injection in #Module
But We’ll create new #Component A only for testing
This #Component will have a method to get that #Module
During tests, we swap the #Component that the application use with our component A
Then things are easy:
Without DaggerMock
In the #Module, instead of return real instance, you just return mockito mock.
With DaggerMock
You declare the type you want to swap and mock it
You can then use the mock.
No need to change the #Module
It inspires by #AutonomousApps 's solution, but the differences are now you don't need to write the #Component, #Module for each test class.
After trying several approaches, this is the only one that worked for me.
I wrote a blog post that explains how to do this just yesterday: https://dev.to/autonomousapps/the-daggerandroid-missing-documentation-33kj
I don't intend to repeat the entire post for this answer (it's hundreds of words and lines of code to properly set up a test harness with Dagger), but to attempt to summarize:
Add a custom application class in the debug source set (I assume it would also work in the androidTest source set, but I have not tried this).
You also need to reference this application in a AndroidManifest.xml in the same source set.
Create a "Test component" in your androidTest class that extends from your production top-level component and build it.
Use that test component to inject your application, which means you've just replaced your entire Dagger dependency graph with a new one you've defined just for the test suite.
Profit.

Categories

Resources