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

Testing with dagger #1052

Open
Dunemaster opened this issue Jan 30, 2018 · 5 comments
Open

Testing with dagger #1052

Dunemaster opened this issue Jan 30, 2018 · 5 comments

Comments

@Dunemaster
Copy link

Dunemaster commented Jan 30, 2018

I would like to discuss testing with dagger (continuing #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
Copy link
Author

Dunemaster commented Jan 30, 2018

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

caltseng commented May 1, 2019

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

luislukas commented Jun 13, 2019

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

vinaygopinath commented Apr 19, 2020

@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
Copy link
Member

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.

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

No branches or pull requests

6 participants