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
}
}
}