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

Way to replace a dependency created using Constructor Injection for all tests using Hilt. #4386

Closed shubhamgarg1 closed 3 months ago

shubhamgarg1 commented 3 months ago

Hilt provides mainly two ways to replace bindings:

1. TestInstallIn

It allows you to replace a module for all test code.

2. BindValue

This helps you to replace a specific binding in a single test case.

If a dependency for a class has been created using Constructor Injection and without any module, there is no way to replace that binding for all the test cases directly.

Can such support be provided in Hilt?

bcorso commented 3 months ago

The general guidance when you have multiple implementations is to create an interface and then @Binds the implementations in modules. For example:

interface Foo

// Production classes
class FooImpl @Inject constructor(...)

@Module
@InstallIn(...)
interface FooModule {
  @Binds fun bind(impl: FooImpl): Foo
}

// Test classes
class FooTestImpl @Inject constructor(...)

@Module
@TestInstallIn(components = ..., replaces = FooModule.class)
interface FooTestModule {
  @Binds fun bind(impl: FooTestImpl): Foo
}
shubhamgarg1 commented 3 months ago

Yes, this would surely work but needs one to define an interface for that class and ends up allowing one to also inject the implementation directly in parts of the code and that wouldn't get replaced in the test cases. In case, we had a functionality to replace all implementations of a class in all tests created directly using constructor injection, that would be great.

bcorso commented 3 months ago

needs one to define an interface for that class

Unless you're just mocking out the behavior, which we only recommend as a last resort (see https://dagger.dev/hilt/testing-philosophy), then you usually want an interface so that you can define two separate implementations. Typically your prod and test implementations are going to be significantly different, e.g. have different dependencies etc.

and ends up allowing one to also inject the implementation directly in parts of the code and that wouldn't get replaced in the test cases

Typically Foo is public whereas FooImpl/FooTestImpl are package-private/internal to prevent others from using the implementation directly. It requires more up-front organization of your code to work properly, but should be a net benefit.

In case, we had a functionality to replace all implementations of a class in all tests created directly using constructor injection, that would be great.

If you still really want to replace a constructor injected class directly, it should already be possible. In Dagger, an @Provides method will take precedence over an @Inject constructor class so you just need to add a module with an @Provides method in your test sources, e.g.

// This is used in production.
class Foo @Inject constructor(...) {...}

// This module is included with your test sources and will override the inject constructor above.
@Module
@InstallIn(SingletonComponent::class)
interface FooTestModule {
  @Provides
  fun provideFoo(): Foo {...}
}
shubhamgarg1 commented 3 months ago

Thanks a lot. I wasn't aware that @Providesmethod takes precedence over @Inject constructor class. This should help a lot in the use case I was looking to solve with it.