Open Dunemaster opened 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
@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?
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)
@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
}
}
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.
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)