question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Cannot create Test component as subclass of Main component when contributed subcomponents are present

See original GitHub issue

So I have my main app component:

@MergeComponent(AppScope::class)
@AppScope
interface AppComponent {
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance application: Application): AppComponent
    }
}

And a subclass in my androidTest folder:

@MergeComponent(AppScope::class)
@AppScope
interface TestAppComponent: AppComponent {
    @Component.Factory
    interface Factory {
        fun create(
            @BindsInstance application: Application,
            @BindsInstance mockInterceptor: MockInterceptor
        ): TestAppComponent
    }
}

This works relatively well, except when i introduce a contributed subcomponent:

@ContributesSubcomponent(
    parentScope = AppScope::class,
    scope = PodOnboardingScope::class
)
@PodOnboardingScope
interface PodOnboardingComponent {
    fun activityComponentFactory(): PodOnboardingActivityComponent.Factory

    @ContributesSubcomponent.Factory
    interface Factory {
        fun create(): PodOnboardingComponent
    }

    @ContributesTo(AppScope::class)
    interface FactoryCreator {
        fun podOnboardingComponentFactory(): Factory
    }
}

This leads to this error:

TestAppComponent.java:9: error: conflicting entry point declarations:
public abstract interface TestAppComponent extends com.xfinity.dh.pods.app.di.AppComponent, com.xfinity.dh.pods.di.PodOnboardingComponent.FactoryCreator, com.xfinity.dh.pods.app.di.AppInjectors, anvil.component.com.xfinity.dh.app.di.testappcomponent.PodOnboardingComponentA.ParentComponent {
                ^
      @org.jetbrains.annotations.NotNull @Override anvil.component.com.xfinity.dh.app.di.testappcomponent.PodOnboardingComponentA.SubcomponentFactory anvil.component.com.xfinity.dh.app.di.testappcomponent.PodOnboardingComponentA.ParentComponent.podOnboardingComponentFactory()
      @org.jetbrains.annotations.NotNull anvil.component.com.xfinity.dh.pods.app.di.appcomponent.PodOnboardingComponentA.SubcomponentFactory anvil.component.com.xfinity.dh.pods.app.di.appcomponent.PodOnboardingComponentA.ParentComponent.podOnboardingComponentFactory()

I have a temporary workaround which is to move my injectors/accessors to their own interfaces in the main app, and look those up in my component registry by their interfaces. Once i do that, i can remove the inheritance, and i generally don’t ever refer to the AppComponent directly unless i’m actually creating it. It works, but it seems odd to have to do this.

Issue Analytics

  • State:closed
  • Created a year ago
  • Comments:5

github_iconTop GitHub Comments

1reaction
mattingercommented, May 2, 2022

Yeah, that’s what i’ve done is to move the injectors to a different interface. However, traditional testing in dagger usually extends the main component to produce the app component, and it works. That’s the only reason I bring this up as an issue is that it’s something i see done a lot in non anvil based dagger.

0reactions
PaulWoitaschekcommented, Jul 22, 2022

To help others here, the solution suggested by @vRallev is good. The key here is that you can still have some form of inheritance if you need it. For example if you swap out an injected Component by a test component.

So if you previously have a component like this:

@AppScope
@MergeComponent(
  scope = AppScope::class,
  dependencies = [DatabaseComponent::class],
)
interface AppComponent {

  fun inject(target: App)

  @Component.Factory
  interface Factory {
    fun create(
      @BindsInstance application: Application,
      databaseComponent: DatabaseComponent,
      networkModule: NetworkModule
    ): AppComponent

    companion object {
      operator fun invoke(): Factory {
        return DaggerAppComponent.factory()
      }
    }
  }
}

And a test component like this:


@AppScope
@MergeComponent(
  scope = AppScope::class,
  dependencies = [DatabaseComponent::class],
)
interface TestComponent : AppComponent {

  val loginUser: LoginUser

  @Component.Factory
  interface Factory {
    fun create(
      @BindsInstance application: Application,
      databaseComponent: DatabaseComponent,
      networkModule: NetworkModule,
    ): TestComponent

    companion object {
      operator fun invoke(): Factory {
        return DaggerTestComponent.factory()
      }
    }
  }
}

You can now extract out an interface:

interface AppComponent {

  fun inject(target: App)
}

And for the production and the test component you extend from that interface:

@AppScope
@MergeComponent(
  scope = AppScope::class,
  dependencies = [DatabaseComponent::class],
  modules = [MobileSharedModule::class]
)
interface ProductionAppComponent : AppComponent {

  @Component.Factory
  interface Factory {
    fun create(
      @BindsInstance application: Application,
      databaseComponent: DatabaseComponent,
      networkModule: NetworkModule
    ): ProductionAppComponent

    companion object {
      operator fun invoke(): Factory {
        return DaggerProductionAppComponent.factory()
      }
    }
  }
}
@AppScope
@MergeComponent(
  scope = AppScope::class,
  dependencies = [DatabaseComponent::class],
  modules = [MobileSharedModule::class]
)
interface TestComponent : AppComponent {

  val loginUser: LoginUser

  @Component.Factory
  interface Factory {
    fun create(
      @BindsInstance application: Application,
      databaseComponent: DatabaseComponent,
      networkModule: NetworkModule,
    ): TestComponent

    companion object {
      operator fun invoke(): Factory {
        return DaggerTestComponent.factory()
      }
    }
  }
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

ContributesSubcomponent issue in testing #459 - GitHub
When creating integration tests with real dagger component, I had created TestAppComponent, TestUserComponent, in each app, with excluded modules which ...
Read more >
How do you override a module/dependency in a unit test with ...
We've to set the test component in App class before the MainActivity is created - because StringHolder is injected in the onCreate callback....
Read more >
Testing with Dagger
One of the benefits of using dependency injection frameworks like Dagger is that it makes testing your code easier. This document explores some...
Read more >
Testing Components – Testing Angular
The TestBed creates and configures an Angular environment so you can test particular application parts like Components and Services safely ...
Read more >
Component testing scenarios - Angular
The purpose of the spec is to test the component, not the service, and real services can be trouble.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found