google / dagger

A fast dependency injector for Android and Java.
https://dagger.dev
Apache License 2.0
17.45k stars 2.02k forks source link

Testing with dagger #1052

Open Dunemaster opened 6 years ago

Dunemaster commented 6 years ago

I would like to discuss testing with dagger (continuing https://github.com/google/dagger/issues/110). As it stands, testing with dagger is hard, requiring a log of boilerplate code and/or altering system design just to facilitate testing, especially when subcomponents and abstract modules are involved (For eaxmple, the approaches described in https://google.github.io/dagger/testing.html ) .

There are frameworks like https://github.com/fabioCollini/DaggerMock, which try to solve the problem, but there have rather limited applicability. For example, DaggerMock does not support abstract modules (thought, I must say, DaggerMock is just an excellent work).

Basically, my requirments for testing fall into two categories:

1) Observe the state of some service in component (which can be an "internal" service, not provided by component interface) 2) Substitute some service in component

Currently, there is not elegant way to do (1) and no way to implement (2) (if you dont count creating separate modules for every single services)

Dunemaster commented 6 years ago

The first solution, which comes to mind is to generate a component designed for testing, with methods to substitute every single provider and to get every service.

More detailed description: 1) Generate a second builder for every dagger Component (call it DaggerTestingComponentBuilder, for example) 2) add methods to substitute every single service in the builder (substituteServiceXXX) 3) add methods to retrieve service instances to generated Component

If no services have been substituted in the builder, the testing component should behave exaclty as production component

P.S. If such (or some other) design is accepted, I may try to implement a PR

caltseng commented 5 years ago

@Dunemaster - I tried creating another component that extends the base component (which is the suggested path), but I am getting Duplicate class errors in compilation because of the sub components. Is this something you've also run into?

luislukas commented 5 years ago

I'm following these steps to override Modules of a Component (using the new Factory approach introduced in dagger 2.22).

The Component looks like:

 @Component(
     modules = [SomeModule::class],
     dependencies = [SessionComponent::class]
 )
 interface MainComponent {
     @Component.Factory
     interface Factory {
         fun create(sessionComponent: SessionComponent, someModule: SomeModule): MainComponent
     }
 }

The Module looks like:

@Module
class SomeModule {

    @VisibleForTesting
    var some : Some = SomeImpl()

    @Provides
    fun some(): Some = some
}

The interface and Implementation:

interface Some {
    fun some(): String
}

class SomeImpl : Some {
    override fun some() = "..."
}

An Injector to help swapping the Implementation later on:

object Injector {

    @VisibleForTesting
    var someModule: SomeModule = SomeModule()

    fun inject(activity: Activity) {
        when (activity) {
            is MainActivity -> {
                DaggerMainComponent.factory()
                    .create(App.sessionComponent(activity), someModule).inject(activity)
            }
        }
    }
}

Then in our MainActivity

class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var some: Some

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Injector.inject(this)
        setContentView(R.layout.activity_main)        
    }
}

Then in your EspressoTest you do the following:

@Test
    fun testSomething_1() {
        Injector.someModule.some = TestSomeImpl_1()
        activityRule.launchActivity(null)
        onView(...).check(matches(isDisplayed()))
    }

@Test
    fun testSomething_2() {
        Injector.someModule.some = TestSomeImpl_2()
        activityRule.launchActivity(null)
        onView(...).check(matches(isDisplayed()))
    }

class TestSomeImpl_1(): Some {
  override fun some() = "1"
}
class TestSomeImpl_2(): Some {
  override fun some() = "2"
}

The idea behind this is to have access to the implementation behind the Module and be able to change it. In other old projects not using the Factory approach I've achieved the same overriding the implementation of the Module + playing with gradle flavours as well, somehow similar to this approach. I would like to know if there's a way not to expose the Module or override the implementation behind SomeImpl and use either object for Module or at least abstract with @Binds - for optimisation purposes. So far I haven't found anything - apart from relaying in gradle swapping SomeImpl.kt file (locking us with just one implementation)

vinaygopinath commented 4 years ago

@luislukas By creating and holding on to SomeImpl in a field, aren't you eagerly loading all dependencies when modules are instantiated?

@Module
class SomeModule {

    @VisibleForTesting
    var some : Some = SomeImpl()

    @Provides
    fun some(): Some = some
}

Also, if SomeImpl requires constructor arguments, this is not an option. You'll need something like

@Module
class SomeModule {
  @VisibleForTesting
  lateinit var some: Some

  @Provides
  fun some(dependencyAOfSomeImpl: DependencyA): Some {
    if (!::some.isInitialized) { // Skip this check if you don't want @Singleton behaviour
      some = SomeImpl(dependencyAOfSomeImpl)
    }

    return some
  }  
}
Chang-Eric commented 3 years ago

I think #186 also has discussion on this topic. We're aware of testing being a problem with Dagger in general and our approach is going to be to improve this with Hilt. Hilt only works for Android right now, but I think we see some of the testing solutions there as eventually being applicable to non-Android cases as well.