DaggerMock
DaggerMock copied to clipboard
Support for injecting mockito Spy objects without overriding modules
Thank you very much for the wonderful library. I have been playing around with it and i came across some limitations recently when i was trying to implement a UI test in androidTest
package. i am using dagger.android
and i want to inject some classes as spy
without specifically creating the test modules manually
Example
What i want to do is that i have aProductRepository
and want to stub a few methods instead of mocking the whole ProductRepository
. I know something like this (Partial mocking) is possible by using spy
instead of mock
. My product repository has 2 dependencies and i want dagger to provide them instead of providing them manually for my spy object. Below is the code for my test project.
CustomTestRunner
class CustomTestRunner : AndroidJUnitRunner() {
@Throws(InstantiationException::class, IllegalAccessException::class, ClassNotFoundException::class)
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
DexOpener.install(this)
return super.newApplication(cl, "com.krazykira.TestApplication", context)
}
override fun onStart() {
RxJavaPlugins.setIoSchedulerHandler { Schedulers.from(AsyncTask.THREAD_POOL_EXECUTOR) }
super.onStart()
}
override fun onDestroy() {
RxJavaPlugins.reset()
super.onDestroy()
}
}
TestApplication
class TestApplication : MyApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerTestAppComponent.builder().create(this)
}
}
TestAppComponent
@Singleton
@Component(modules = [
AndroidSupportInjectionModule::class,
ActivityBuilder::class,
PresenterModule::class,
SpyRepositoryModule::class, // Don't like this instead would prefer RepositoryModule
SourceModule::class,
UtilsModule::class
])
interface TestAppComponent : AndroidInjector<MyApplication> {
@Component.Builder
abstract class Builder : AndroidInjector.Builder<MyApplication>() {
abstract fun presenterModule(presenterModule: PresenterModule): Builder
abstract fun mockRepositoryModule(mockRepositoryModule: MockRepositoryModule): Builder
// instead would rather use abstract fun repositoryModule(repositoryModule: RepositoryModule): Builder
abstract fun sourceModule(sourceModule: SourceModule): Builder
abstract fun utilsModule(utilsModule: UtilsModule): Builder
}
//Will provide the ProductRepository instance used in the app to the test.
fun productRepository(): ProductRepository
}
TestDaggerMockRule
class TestDaggerMockRule(useMocks: Boolean = true) : DaggerMockRule<TestAppComponent>(
TestAppComponent::class.java,
PresenterModule(),
SpyRepositoryModule(), // instead would like to use RepositoryModule()
SourceModule(),
UtilsModule()
) {
private val app: TestApplication= InstrumentationRegistry.getInstrumentation()
.targetContext
.applicationContext as TestApplication
init {
customizeBuilder { builder: TestAppComponent.Builder ->
builder.seedInstance(app)
return@customizeBuilder builder
}
set { component -> component.inject(app) }
}
}
I am unable to find a way to do this easily using DaggerMock
. The only option i see is manually overriding the SpyRepositoryModule
which provides SpyProductRepository
. Can you tell me if there is any other simple way to do this without rewriting the modules ? (Which will provide me spies instead of real objects)
SpyProductRepository
I would really like to not do this
@Module
open class SpyProductRepository {
@Singleton
@Provides
fun provideProductRepository(networkSource: NetworkSource, diskSource: DiskSource): ProductRepository {
return spy(ProductRepositoryImpl(networkSource, diskSource))
}
}
Also another thing, If i remove open
from SpyProductRepository
which is located inside the androidTest
package then i am getting the following error. It works fine if i move this class to main
package where the app code resides then the error goes away. Super confusing :(
Mockito cannot mock/spy because :
- final class
at it.cosenonjaviste.daggermock.ModuleOverrider.override(ModuleOverrider.java:69)
at it.cosenonjaviste.daggermock.DaggerMockRule.initComponent(DaggerMockRule.java:238)
at it.cosenonjaviste.daggermock.DaggerMockRule.setupComponent(DaggerMockRule.java:130)
at it.cosenonjaviste.daggermock.DaggerMockRule.access$000(DaggerMockRule.java:36)
at it.cosenonjaviste.daggermock.DaggerMockRule$1.evaluate(DaggerMockRule.java:110)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at android.support.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
at android.support.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:384)
at com.sherazkhilji.ambosstest.support.CustomTestRunner.onStart(CustomTestRunner.kt:23)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2074)
Suggestion
Maybe the DaggerMockRule
can take an boolean as constructor to provide spies for all dependencies or provide a method where we could override
to provide all the dependencies that we could spy
Hi, right now I think that the only solution available is to declare all three fields in the test, obviously it's a decent workaround only if there aren't other dependencies. Something like this:
val networkSource = NetworkSource()
val diskSource = DiskSource()
val productRepository: ProductRepository = spy(ProductRepositoryImpl(networkSource, diskSource))
DaggerMock will replace all the three objects in the module and it should work. However this is an use case that can happen and something in the rule that allows to decorate an existing object can be useful. I'll try to add it in a future release, thanks for the suggestion!
About the final class issue, this is a problem related to the mockito usage on Kotlin, my suggestion to solve it is to use mockito inline dependency in JVM tests or Kotlin all open compiler plugin in Espresso tests.
Thanks for the response, maybe i can help with a PR once i have some time.
About the final class issue, this is a problem related to the mockito usage on Kotlin, my suggestion to solve it is to use mockito inline dependency in JVM tests or Kotlin all open compiler plugin in Espresso tests.
I don't understand whats the difference and why it works in main
package but doesn't work on androidTest
package. I am already using dexOpener and i don't think it is an issue caused by it. Maybe shed some light on whats difference it makes when the file is in main
package and when it is in androidTest
package
The instrumentations test are not executed on the jvm so mockito inline is not enough. You need to use kotlin all open or something similar. I think that the reason is something related to the mockito implementation and the differences between jvm and dalvik/art.
The instrumentations test are not executed on the jvm so mockito inline is not enough
i am not using mockito inline, rather using Dexopener. DaggerMockRule is what throws this error.
@fabioCollini i added a PR which adds the spy feature to DaggerMock
The error is thrown by DaggerMock because internally it uses Mockito. DexOpener should work, maybe you are defining your module in a package that it's not managed by DexOpener. You can find more details here: https://github.com/tmurakami/dexopener#limitations
The package name of both the main
and androidTest
package are the same and the path for DexOpener
is also correct. Maybe i should share the code with you to give you a better overview