fabioCollini / DaggerMock

A JUnit rule to easily override Dagger 2 objects
Apache License 2.0
1.16k stars 91 forks source link

Support for injecting mockito Spy objects without overriding modules #81

Open krazykira opened 6 years ago

krazykira commented 6 years ago

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

fabioCollini commented 6 years ago

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.

krazykira commented 6 years ago

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

fabioCollini commented 6 years ago

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.

krazykira commented 6 years ago

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.

krazykira commented 6 years ago

@fabioCollini i added a PR which adds the spy feature to DaggerMock

fabioCollini commented 6 years ago

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

krazykira commented 6 years ago

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