Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for injecting mockito Spy objects without overriding modules #81

Open
krazykira opened this issue Aug 15, 2018 · 7 comments
Open

Comments

@krazykira
Copy link

krazykira commented Aug 15, 2018

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Author

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

@fabioCollini
Copy link
Owner

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
Copy link
Author

krazykira commented Aug 24, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants