3

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.

StuartDTO
  • 783
  • 7
  • 26
  • 72
  • 1
    You can't override components. You need to use your "test component" in tests, and "real component" in production code. However, from the code you posted, it's hard to say where you're going wrong in your dagger setup. – Shark Apr 15 '21 at 09:18
  • Yes but if my subcomponent has a `modules=[...]` how do I fake these? Let me edit my quesiton with clear code – StuartDTO Apr 15 '21 at 09:45
  • @Shark edited my question, the thing is how to "override" the modules on my production subcomponent and use the mock one on my mockSubcomponent – StuartDTO Apr 15 '21 at 09:50
  • 1
    well, have your production subcomponent not use a hardcoded module list? lemme try posting something and see whether you catch the idea – Shark Apr 15 '21 at 12:51
  • are you binding the `FeatureOneComponent` to the ApplicationComponent? Are you sure it's binding the TestF1Component instead of the ProductionF1Component there? – Shark Apr 15 '21 at 14:40
  • What do you mean with binding? It's hard to test because until all of this is not fixed my component is not generated. how can I test this case you are asking? – StuartDTO Apr 15 '21 at 15:10
  • post the application component as well... also, why is FeatureOneComponent a real component and not just a module? – Shark Apr 16 '21 at 08:24
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/231211/discussion-between-stuartdto-and-shark). – StuartDTO Apr 16 '21 at 12:45
  • I agree with @Shark: you can't override components. The whole advantage of Dagger is that it implements your graph with compile-time code injection, so if you want to make changes to that in your test, you're going to need some other kind of indirection, or you're going to need to make a separate test component. Dagger has [official advice about testing](https://dagger.dev/dev-guide/testing.html) that further describes your options. – Jeff Bowman Apr 16 '21 at 17:52
  • Yes but I did not find the way to a test use my mockSubcomponent... how can achieve this? – StuartDTO Apr 17 '21 at 09:17
  • @JeffBowman I do have a separate TestComponent, the thing is that everything is mocked but the classes I've added as a module on my Subcomponent... My real problem is how can I mock the module that is inside the subcomponent? – StuartDTO Apr 17 '21 at 19:02
  • You might take a look at [this thread](https://stackoverflow.com/q/51110120/3290339) where I proposed a solution on mocking presenters in instrumentation testing. There is a reference to a sample project in the answer. – Onik Apr 23 '21 at 18:02
  • I found your solution before but you are not "overriding" a sub-component – StuartDTO Apr 23 '21 at 20:07

3 Answers3

1

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.

0xAliHn
  • 18,390
  • 23
  • 91
  • 111
  • This is another way, I don't want to inject on my test, I'm trying to create a TestSubcomponent, check on my question the `@Subcomponent(modules=[ActivityOneModule::class]` I want this module to mock, but I can not mock that subcomponent the other things are mocked. – StuartDTO Apr 18 '21 at 07:26
1

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.

Skizo-ozᴉʞS ツ
  • 19,464
  • 18
  • 81
  • 148
0

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?

Shark
  • 6,513
  • 3
  • 28
  • 50
  • @StuartDTO basically, the TestComponent will have modules that provide mocks or test variants of (some of) their production counterparts, and thats just showing the general direction in which you should be going. This isn't a step by step guide or a solution to your question, just demonstrates the idea of how to bypass the problem you're currently having - binding production modules instead of test modules in your component. – Shark Apr 15 '21 at 13:12
  • Let me update the question with matched codes. – StuartDTO Apr 15 '21 at 13:27
  • could you re-check the question? I've edited it, it compiles and it works but I'm missing the part where I mock the modules of the subcomponent... – StuartDTO Apr 17 '21 at 19:03