Hilt en unit tests — no lo necesitás

En los unit tests (src/test/), Hilt generalmente no está involucrado. Instanciás las clases directamente pasando los Fakes y Mocks como parámetros. Hilt es para inyección en runtime — en tests unitarios vos controlás la construcción:

// Unit test SIN Hilt — directo y simple
class ProductosViewModelTest {

    @get:Rule val mainDispatcherRule = MainDispatcherRule()

    private val fakeRepo = FakeProductoRepository()  // instancia directa
    private val viewModel = ProductosViewModel(fakeRepo)  // vos construís el objeto

    @Test
    fun `cargar productos funciona`() = runTest { /* ... */ }
}
// No necesitás @HiltAndroidTest, no necesitás reglas de Hilt, no necesitás módulos.
// Esto es correcto y preferible para unit tests.

Hilt en tests es para tests instrumentados@HiltAndroidTest y @TestInstallIn son herramientas para tests en src/androidTest/ donde la app se instala en un dispositivo real y Hilt está activo. Para unit tests, construí los objetos a mano.

@HiltAndroidTest — la base de los tests instrumentados con Hilt

// Un test instrumentado con Hilt sigue esta estructura:
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class ProductosFragmentTest {

    // Esta regla inicializa Hilt antes de cada test
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    // Si testás Activities/Fragments, también necesitás ActivityScenario
    // @get:Rule(order = 1)
    // val activityRule = ActivityScenarioRule(MainActivity::class.java)

    // Hilt puede inyectar dependencias directamente en el test
    @Inject
    lateinit var repository: ProductoRepository

    @Before
    fun setUp() {
        hiltRule.inject()  // Esto activa la inyección en este objeto
    }

    @Test
    fun `repositorio inyectado es el fake`() {
        // Si configuraste @TestInstallIn correctamente,
        // repository aquí es el FakeProductoRepository
        assertThat(repository).isInstanceOf(FakeProductoRepository::class.java)
    }
}

@TestInstallIn — reemplazar módulos en tests

La herramienta más poderosa: reemplazar el módulo de producción con uno de testing en todos los tests instrumentados:

// Módulo de producción (en src/main/):
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds @Singleton
    abstract fun bindProductoRepository(
        impl: ProductoRepositoryImpl
    ): ProductoRepository
}

// Módulo de test (en src/androidTest/):
// Reemplaza RepositoryModule con FakeRepositoryModule en TODOS los tests
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [RepositoryModule::class]   // ← reemplaza el módulo de producción
)
@Module
abstract class FakeRepositoryModule {

    @Binds @Singleton
    abstract fun bindFakeRepository(
        fake: FakeProductoRepository
    ): ProductoRepository
}

// FakeProductoRepository también necesita ser @Inject constructible:
class FakeProductoRepository @Inject constructor() : ProductoRepository {
    private val productos = mutableListOf<Producto>()
    // ...
}

// Ahora TODOS los @HiltAndroidTest del proyecto usan FakeProductoRepository
// sin necesidad de configurarlo en cada test individualmente

@BindValue — reemplazar una dependencia específica

Cuando necesitás un valor diferente en un test específico sin crear un módulo entero:

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class LoginTest {

    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    // @BindValue reemplaza el binding de ese tipo en este test específico
    @BindValue
    val fakeRepo: ProductoRepository = FakeProductoRepository().apply {
        setProductos(listOf(productoTest1, productoTest2))
    }

    // También podés inyectar un mock:
    @BindValue
    val mockAnalytics: AnalyticsTracker = mockk(relaxed = true)

    @Before fun setUp() { hiltRule.inject() }

    @Test
    fun `login con credenciales validas muestra productos`() {
        // El fakeRepo tiene 2 productos pre-cargados
        // El mockAnalytics no hace nada pero registra las llamadas
    }
}

TestApplication — Application class para tests

Hilt necesita una Application con @HiltAndroidApp. Para tests instrumentados, creás una Application de test:

// src/androidTest/java/ar/pensa/miapp/HiltTestApplication.kt
@HiltAndroidApp
class HiltTestApplication : Application()

// Configurar en el runner personalizado:
// src/androidTest/java/ar/pensa/miapp/HiltTestRunner.kt
class HiltTestRunner : AndroidJUnitRunner() {
    override fun newApplication(
        cl: ClassLoader?,
        className: String?,
        context: Context?
    ): Application {
        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}

// Registrar en build.gradle:
android {
    defaultConfig {
        testInstrumentationRunner = "ar.pensa.miapp.HiltTestRunner"
    }
}

ViewModel con Hilt en tests instrumentados

// En tests instrumentados, el ViewModel se obtiene a través del Fragment/Activity
// El grafo de Hilt lo construye con las dependencias de test

@HiltAndroidTest
class ProductosViewModelInstrumentedTest {

    @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)

    @BindValue
    val fakeRepo: ProductoRepository = FakeProductoRepository()

    @Before fun setUp() { hiltRule.inject() }

    @Test
    fun `viewmodel inyectado usa el fake repository`() {
        val scenario = launchFragmentInContainer<ProductosFragment>()

        scenario.onFragment { fragment ->
            val viewModel = fragment.viewModel  // acceder al ViewModel del Fragment
            // El ViewModel fue construido con fakeRepo por Hilt
            viewModel.cargar()
            assertThat(viewModel.uiState.value.productos).hasSize(0) // fakeRepo vacío
        }
    }
}