square / anvil

A Kotlin compiler plugin to make dependency injection with Dagger 2 easier.
Apache License 2.0
1.32k stars 82 forks source link

Generating Component.Builder methods #389

Closed mattinger closed 3 years ago

mattinger commented 3 years ago

Im trying to use anvil with daggermock as a testing framework. However, the issue i'm running into is that daggermock doesn't really like Component.Factory, which is understandable. So i would then use a Component.Builder. However, because the list of modules is not known to me when writing the code, I can only create a builder which will internally create all the required modules:

@Component.Builder
interface Builder {
  @BindsInstance fun context(context: Context): Builder
   fun build(): MyComponent
}

Due to this, daggermock can't override the modules because you can't create an instance and pass it to the builder (which is my understanding of how daggermock works).

What i'd love is to be able to generate a builder in anvil from something like this:

@MergeComponent.Builder
interface MergeBuilder {
   @BindsInstance fun context(context: Context): Builder
   fun build(): MyComponent
}

And have anvil recognize that, and create a full fledged builder with setters for all the collected modules.

vRallev commented 3 years ago

I haven't heard of daggermock before. I don't see us implementing what you're asking for. However, Anvil provides extension points to create your own custom code generator and I think what you're asking for could be solved this way. Please take a look at the documentation.

mattinger commented 3 years ago

I've taken a brief look and I'm wondering if this will even work for me. To be honest, we need to hook into the generation of the actual class annotated with @Component so that we can create the inner @Component.Builder or @Component.Factory interface. Is this even possible with the compiler api?

RBusarow commented 3 years ago

Component generation is done via Dagger, after Anvil's processing is all done.

Am I understanding this correctly? If you wanted to create a binding for MyService and MyServiceImpl, you might want to write this:

@ContributesBinding(MyScope::class)
class MyServiceImpl @Inject constructor(...) : MyService

You can't override this "module" because there isn't really a module.

* Technically, there is a module. Anvil creates a single, potentially giant module with all the bindings for each component. But if I understand DaggerMock correctly, it creates mocks for every binding in the module, so you wouldn't want to do that here.

Simple workaround

Instead of using @ContributesBinding for that binding, you could just create a normal module:

class MyServiceImpl @Inject constructor() : MyService

@Module
@ContributesTo(MyScope::class)
interface MyServiceModule {
  @get:Binds
  val MyServiceImpl.bindMyService: MyService
}

And then DaggerMock and Dagger should work normally.

Idiomatic Anvil

DaggerMock supplies the mock bindings using the module overrides, but Anvil does the same thing using the "replace" functionality referenced in @Contributes___ annotations.

Instead of creating normal @Module interfaces for bindings in production code, you can also just create fake/mock modules:

// in normal production code
@ContributesBinding(MyScope::class)
class MyServiceImpl @Inject constructor(...) : MyService

// in the test directory, or a test-specific module
@Module
@ContributesTo(MyScope::class, replaces = [MyServiceImpl::class])
object FakeServiceModule {
  @Provides
  @Singleton // the singleton scope is important
  fun provideMockService(): MyService = mockk<MyService>(relaxed = true) // taking liberties here and using MockK
}

With the above code, you don't need DaggerMock. So long as FakeServiceModule is in its classpath somewhere, you could just write something like this:

// Make this a communal thing per-module.  
// For each new test class, add a new overloaded inject(...) function
@Singleton
@MergeComponent(MyScope::class)
interface TestsComponent {
  fun inject(test: RepositoryTest)
}

class RepositoryTest {
  // subject under test
  @Inject lateinit var repository: Repository
  // mocked
  @Inject lateinit var mockService: MyService

  @Before fun before() {
    DaggerTestsComponent.factory()
      .create()
      .inject(this)
  }

  @Test fun `testing things`() { 
    every { mockService.getStuff() } returns ...

    repository.getStuff() shouldBe ...
  }
}
vRallev commented 3 years ago

I've taken a brief look and I'm wondering if this will even work for me. To be honest, we need to hook into the generation of the actual class annotated with @component so that we can create the inner @Component.Builder or @Component.Factory interface. Is this even possible with the compiler api?

I'd suggest doing what @RBusarow suggested above: using Anvil's replaces feature. We rely on that heavily in tests.

To answer your question: The simple API that Anvil exposes through its CodeGenerator interface doesn't allow you to modify code. It only allows you to generate new classes in new files. There are compiler APIs to modify existing code, but Anvil doesn't expose them and makes little use of them internally. You'd be on your own to create your own compiler plugin.

mattinger commented 3 years ago

@vRallev I like the replaces feature, but would that even work in an espresso test where you are building two separate apk files? Would it even consider any modules specified in the androidTest source root when building the application apk?

vRallev commented 3 years ago

You're supposed to replace bindings from the main source set in the androidTest source set. Not the other way around.

vRallev commented 3 years ago

Closing this for now.