Using Dagger with Espresso

Viewed 555

I'm planning to create Espresso tests on my app multi-module, and I'm about to create the first Espresso test, but what I'm seeing is that on my app I do not have an AppComponent where I can fake it. Since I want to add the test on my feature-module, I'll create the TestApp, TestRunner there from now.

What I have on my feature-module is a FeatureComponent that is injected via ComponentFactory from the App, so what I thought is to create a class like this :

@Component (
     dependencies = [ MoreComponents::class],
     modules = [ DataSourceModule::class ]
)
interface FeatureOneComponent { 

    fun activityOneSubComponent(): FeatureOneActivity.Component.Factory
    fun activityTwoSubComponent(): FeatureTwoActivity.Component.Factory

    @Component.Factory
    interface Factory {
        fun create(
            dependencies
        ):FeatureOneComponent
    }
}

interface FeatureOneProvider {
    fun getFeatureOneComponent(): FeatureOneComponent
}


///ACTIVITY

class FeatureOneActivity : AppCompatActivity() {

    //this comes from Subcomponent is what I want to MOCK 
    @Inject lateinit var presenter

    //these comes from the factory and I have it mocked
    @Inject lateinit var manager

    override fun onCreate(){
        (applicationContext as FeatureOneProvider).getFeatureOneComponent().activityOneSubComponent().create(this).inject(this)
    }
}

@Subcomponent(modules = [ActivityOneModule::class]) <--- THIS I WANT TO MOCK
interface Component {
    fun inject(activity: FeatureOneActivity)

    @SubComponent.Factory
    interface Factory {
        fun create(@BindsInstance activity: FeatureOneActivity): Component
    }
}

@Module
interface ActivityOneModule {
    @Binds
    fun bindPresenter(impl: PresenterImpl): Contract.Presenter    
}

TEST

class MyTestApp : Application(), FeatureOneProvider {

    override fun getFeatureOneComponent(): FeatureOneComponent {
        return DaggerMockFeatureOneComponent.create()
    }
}

@Component(
    modules = [MockFeatureOneModules::class]
)
interface MockFeatureOneComponent : FeatureOneComponent {
   

    //I NEED TO MOCK THE SUBCOMPONENT WITH `MockSubcomponent`
}

@Component 
object MockFeatureOneModules {

    @Provides
    fun providesManager() : MyManager = mock(MyManager)
}

//I want to use this module to replace the subcomponent of my activity
@Module
object MockSubcomponent() {
  @Provides
  fun providesFakePresenter() : FeatureOneContract.Presenter = mock { FeatureOneContract.Presenter::class.java }
}

To better understand the problem

When I run my test and I put a debugger point I see everything is mocked but the Presenter, and that's because the presenter is in

@Subcomponent(modules = [ActivityOneModule::class]
interface Component {
    fun inject(activity: FeatureOneActivity)

    @SubComponent.Factory
    interface Factory {
        fun create(@BindsInstance activity: FeatureOneActivity): Component
    }
}

@Module
interface ActivityOneModule {
    @Binds
    fun bindPresenter(impl: PresenterImpl): Contract.Presenter    
}

And in my test component I don't have access to "override" this subcomponent so everything is mocked but this subcomponent and I need this mocked.

3 Answers

I don't know if that's the best idea but if I did not misunderstand you you want this Presenter to return a mock {}. The changes you could do are :

  1. In your TestComponent change interface to abstract class
  2. Duplicate your subcomponent and extends from the real one
@Component(
    modules = [MockFeatureOneModules::class]
)
abstract class MockFeatureOneComponent : FeatureOneComponent {
   

    abstract fun subcomponent() : MockComponent.FactoryMock

    override fun activityOneSubComponent(): FeatureOneActivity.Component.Factory {
        return subcomponent()
    }

    @Subcomponent
    interface MockComponent : FeatureOneActivity.Component { 
       @Subcomponent.Factory
       interface FactoryMock : FeatureOneActivity.Component.Factory {
         override fun create....
       }
    }
}

And that's it, it should work.

So as far as i get it, you have multiple modules, components and subcomponents, but since most of their names don't quite match up in the code you posted, nor you posted the error log, i have to guess whats going wrong where.

Instead, how about something like this.

public interface SomeComponent {

    WhateverClass1 provideWhatever1();
    WhateverClass2 provideWhatever2();
    WhateverRepository1 provideWhateverRepository1();
    SomeOtherComponent getSomeOtherComponent();
   // and so on and on
}

and then have your production component look something like this:

@SomeComponentSingleton
@Component(modules = { ... },
        dependencies = { ... })
public interface SomeProductionScopedComponent extends SomeComponent {

    @Component.Builder
    interface Builder {
       // you know best what needs to go here

       SomeProductionScopedComponent build();
    }
}

and this

@SomeComponentSingleton
@Component(dependencies = SomeComponent.class, modules =
        { ... })
public interface TestSomeComponent {

    @Component.Builder
    interface Builder {
        Builder bindCoreComponent(SomeComponent coreComponent);

        @BindsInstance
        Builder bindContext(Context context);

        TestSomeComponent build();
    }
}

then, given that you're somewhat manually instantiating the components with these Builders, as long as they depend on the interface (SomeComponent), you should be able to bind a ProductionSomeComponent or a TestSomeComponent into your module.

Makes sense or gives some hint?

Your example code is pretty complex to understand and the actual problem. But what I understand you want to setup expresso test for your feature module and you need to setup dagger component for it.

So, I can give you some guidelines and example code so that you can follow and setup your dagger architecture for your espresso test very simply.

First of all, you need setup/create your App for espresso test like this:

class MyTestApplication : MyApplication() {

    //Call this from MyApplication onCreate()
    override fun initDaggerGraph() { 
        component = DaggerTestAppComponent.builder()
            .application(this)
            .appLifecycle(appLifecycle)
            .build()
        component.inject(this)
    }
}

Then create your Test app component like this:

//Add all of your dependent modules in this TestAppModule
@Component(modules = [TestAppModule::class])
interface TestAppComponent : AppComponent {

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: Application): Builder

        @BindsInstance
        fun appLifecycle(appLifecycle: AppLifecycle): Builder

        fun build(): TestAppComponent
    }

    fun inject(activityTest: SomeActivityTest) //Your activity where to inject
}

Also, make sure to initialize your component in your Test activity class when launching the activity like this:

val component = MyApplication.instance.component as TestAppComponent
component.inject(this)

Now you have done all the setup and your dependency should resolve as well as your espresso test should work.

Related