Guía de prueba de Hilt

Uno de los beneficios de usar frameworks de inyección de dependencias, como Hilt, es que facilita la prueba del código.

Pruebas de unidades

No es necesario usar Hilt para las pruebas de unidades, ya que, cuando se prueba una clase que usa la inyección de constructor, no necesitas usar Hilt para crear una instancia de esa clase. En su lugar, puedes llamar directamente a un constructor de clase pasando dependencias falsas o simuladas, tal como lo harías si el constructor no estuviera anotado:

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

class AnalyticsAdapterTest {

  @Test
  fun `Happy path`() {
    // You don't need Hilt to create an instance of AnalyticsAdapter.
    // You can pass a fake or mock AnalyticsService.
    val adapter = AnalyticsAdapter(fakeAnalyticsService)
    assertEquals(...)
  }
}

Lo mismo se aplica a las clases de ViewModel que se obtienen llamando a hiltViewModel() en tus elementos componibles. En las pruebas de unidades, construye el ViewModel directamente con objetos simulados. Para obtener información sobre cómo fluye el estado desde un ViewModel hacia los elementos componibles, consulta El estado y Jetpack Compose y Dónde elevar el estado.

Pruebas de extremo a extremo

En el caso de las pruebas de integración, Hilt inserta las dependencias como lo haría en tu código de producción. Las pruebas con Hilt no requieren mantenimiento, ya que se genera automáticamente un nuevo conjunto de componentes para cada prueba.

Cómo agregar dependencias de prueba

Para usar Hilt en tus pruebas, incluye la dependencia hilt-android-testing en tu proyecto:

dependencies {
    // For Robolectric tests.
    testImplementation("com.google.dagger:hilt-android-testing:2.57.1")
    kspTest("com.google.dagger:hilt-android-compiler:2.57.1")

    // For instrumented tests.
    androidTestImplementation("com.google.dagger:hilt-android-testing:2.57.1")
    kspAndroidTest("com.google.dagger:hilt-android-compiler:2.57.1")

    // Compose UI test rule.
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

}

Cómo configurar la prueba de IU

Debes anotar cualquier prueba de IU que use Hilt con @HiltAndroidTest. Esta anotación es responsable de generar los componentes de Hilt para cada prueba.

Además, debes agregar HiltAndroidRule a la clase de prueba. Administra el estado de los componentes y se usa para realizar la inyección en tu prueba:

@HiltAndroidTest
class SettingsScreenTest {

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

    @get:Rule(order = 1)
    val composeRule = createAndroidComposeRule<HiltTestActivity>()

    // Compose UI tests here.
}

Luego, tu prueba necesita información sobre la clase Application que Hilt genera automáticamente.

Para permitir que Hilt inserte dependencias, debes crear una actividad vacía llamada HiltTestActivity en tu conjunto de orígenes androidTest y anotarla con @AndroidEntryPoint. Luego, createAndroidComposeRule usa esta actividad como host para tu contenido componible.

Aplicación de prueba

Debes ejecutar pruebas instrumentadas que usen Hilt en un objeto Application que sea compatible con Hilt. La biblioteca proporciona un objeto HiltTestApplication para usar en pruebas. Si tus pruebas necesitan una aplicación de base diferente, consulta Aplicación personalizada para pruebas.

Debes configurar tu aplicación de prueba para que se ejecute en tus pruebas instrumentadas o pruebas de Robolectric. Las siguientes instrucciones no se aplican solo a Hilt, sino que son lineamientos generales sobre cómo especificar una aplicación personalizada para ejecutar en pruebas.

Cómo configurar la aplicación de prueba en pruebas instrumentadas

Para usar la aplicación de prueba de Hilt en pruebas instrumentadas, debes configurar un nuevo ejecutor de pruebas. De esta manera, Hilt funcionará en todas las pruebas instrumentadas de tu proyecto. Completa los pasos siguientes:

  1. Crea una clase personalizada que extienda AndroidJUnitRunner en la carpeta androidTest.
  2. Anula la función newApplication y pasa el nombre de la aplicación de prueba de Hilt que se generó.
// A custom runner to set up the instrumented application class for tests.
class CustomTestRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}

Luego, configura este ejecutor de pruebas en tu archivo de Gradle como se describe en la guía de pruebas de unidades instrumentadas. Asegúrate de usar la ruta de clase completa:

android {
    defaultConfig {
        // Replace com.example.android.dagger with your class path.
        testInstrumentationRunner = "com.example.android.dagger.CustomTestRunner"
    }
}
Cómo configurar la aplicación de prueba en las pruebas de Robolectric

Si usas Robolectric para probar tu capa de IU, puedes especificar qué aplicación deseas usar en el archivo robolectric.properties:

application = dagger.hilt.android.testing.HiltTestApplication

Como alternativa, puedes configurar la aplicación en cada prueba por separado con la anotación @Config de Robolectric:

@HiltAndroidTest
@Config(application = HiltTestApplication::class)
class SettingsScreenTest {

  @get:Rule
  var hiltRule = HiltAndroidRule(this)

  // Robolectric tests here.
}

Funciones de prueba

Una vez que Hilt esté listo para que lo uses en tus pruebas, puedes usar varias funciones para personalizar el proceso de prueba.

Cómo insertar tipos en las pruebas

Si deseas insertar tipos en una prueba, usa @Inject para la inyección de campo. Para indicarle a Hilt que propague los campos @Inject, llama a hiltRule.inject().

Consulta el siguiente ejemplo de una prueba instrumentada:

@HiltAndroidTest
class SettingsScreenTest {

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

    @get:Rule(order = 1)
    val composeRule = createAndroidComposeRule<HiltTestActivity>()

    @Inject
    lateinit var analyticsAdapter: AnalyticsAdapter

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

    @Test
    fun settingsScreen_showsTitle() {
        composeRule.setContent {
            SettingsScreen()
        }
        composeRule.onNodeWithText("Settings").assertIsDisplayed()
        // analyticsRepository is available here.
    }
}

Cómo reemplazar una vinculación

Si necesitas insertar una instancia falsa de una dependencia, debes indicarle a Hilt que no use la vinculación que usó en el código de producción y que use una diferente. Para reemplazar una vinculación, debes reemplazar el módulo que la contiene con un módulo de prueba que contenga las vinculaciones que deseas usar en la prueba.

Por ejemplo, supongamos que tu código de producción declara una vinculación para AnalyticsService de la siguiente manera:

@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {

  @Singleton
  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

Para reemplazar la vinculación de AnalyticsService en las pruebas, crea un nuevo módulo de Hilt en la carpeta test o androidTest con la dependencia falsa y anótala con @TestInstallIn. Todas las pruebas en esa carpeta se insertarán con la dependencia falsa.

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [AnalyticsModule::class]
)
abstract class FakeAnalyticsModule {

  @Singleton
  @Binds
  abstract fun bindAnalyticsService(
    fakeAnalyticsService: FakeAnalyticsService
  ): AnalyticsService
}

Dado que los elementos componibles suelen consumir estas dependencias de forma indirecta a través de un ViewModel obtenido con hiltViewModel(), reemplazar la vinculación en Hilt es suficiente. El elemento componible en prueba recoge el elemento falso automáticamente.

Reemplaza una vinculación en una sola prueba

Para reemplazar una vinculación en una sola prueba en lugar de todas las pruebas, desinstala un módulo de Hilt de una prueba con la anotación @UninstallModules y crea un módulo de prueba nuevo dentro de la prueba.

Siguiendo el ejemplo de AnalyticsService de la versión anterior, primero, comienza por indicarle a Hilt que ignore el módulo de producción con la anotación @UninstallModules en la clase de prueba:

@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsScreenTest { ... }

Luego, debes reemplazar la vinculación. Crea un módulo nuevo dentro de la clase de prueba, en el que se defina la vinculación de prueba:

@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsScreenTest {

  @Module
  @InstallIn(SingletonComponent::class)
  abstract class TestModule {

    @Singleton
    @Binds
    abstract fun bindAnalyticsService(
      fakeAnalyticsService: FakeAnalyticsService
    ): AnalyticsService
  }

  // ...
}

Con esto, solo se reemplaza la vinculación de una clase de prueba. Si deseas reemplazar la vinculación de todas las clases de prueba, usa la anotación @TestInstallIn de la sección anterior. Como alternativa, puedes colocar la vinculación de prueba en el módulo test para pruebas de Robolectric o en el módulo androidTest para pruebas instrumentadas. Se recomienda usar @TestInstallIn siempre que sea posible.

Cómo vincular valores nuevos

Usa la anotación @BindValue para vincular fácilmente los campos de tu prueba en el gráfico de dependencia de Hilt. Anota un campo con @BindValue y se vinculará bajo el tipo de campo declarado con cualquier calificador que esté presente para ese campo.

En el ejemplo de AnalyticsService, puedes reemplazar AnalyticsService por uno falso usando @BindValue:

@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsScreenTest {

  @BindValue @JvmField
  val analyticsService: AnalyticsService = FakeAnalyticsService()

  ...
}

Con esto, se simplifica el reemplazo de una vinculación y la referencia a una vinculación en tu prueba, ya que te permite realizar ambas acciones al mismo tiempo.

@BindValue funciona con calificadores y otras anotaciones de prueba. Por ejemplo, si usas bibliotecas de prueba como Mockito, puedes usarlo en una prueba de Robolectric de la siguiente manera:

...
class SettingsScreenTest {
  ...

  @BindValue @ExampleQualifier @Mock
  lateinit var qualifiedVariable: ExampleCustomType

  // Robolectric tests here
}

Si necesitas agregar una vinculación múltiple, puedes usar las anotaciones @BindValueIntoSet y @BindValueIntoMap en lugar de @BindValue. En cuanto a @BindValueIntoMap, también requiere que anotes el campo con una anotación de clave del mapa.

Casos especiales

Hilt también ofrece funciones para admitir casos prácticos no estándar.

Aplicación personalizada para pruebas

Si no puedes usar HiltTestApplication porque tu aplicación de prueba necesita extender otra aplicación, anota una nueva clase o interfaz con @CustomTestApplication y pasa el valor de la clase de base que deseas que la aplicación generada por Hilt extienda.

@CustomTestApplication generará una Application lista para pruebas con Hilt que extiende la aplicación que pasaste como parámetro.

@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

En el ejemplo, Hilt genera una Application, llamada HiltTestApplication_Application, que extiende la clase BaseApplication. En general, el nombre de la aplicación que se genera es el nombre de la clase anotada que se agregó con _Application. Debes configurar la aplicación de prueba generada por Hilt para que se ejecute en tus pruebas instrumentadas o pruebas de Robolectric como se describe en Aplicación de prueba.

Varios objetos TestRule en tu prueba instrumentada

Las pruebas de IU de Compose ya combinan HiltAndroidRule con una regla de prueba de Compose, como createAndroidComposeRule. Si tienes objetos TestRule adicionales, asegúrate de que HiltAndroidRule se ejecute primero. Declara el orden de ejecución con el atributo order en @Rule:

@HiltAndroidTest
class SettingsScreenTest {

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

  @get:Rule(order = 1)
  val composeRule = createAndroidComposeRule<HiltTestActivity>()

  @get:Rule(order = 2)
  val otherRule = SomeOtherRule()

  // UI tests here.
}

Como alternativa, puedes unir las reglas con RuleChain y colocar HiltAndroidRule como la regla externa.

@HiltAndroidTest
class SettingsScreenTest {

  @get:Rule
  var rule = RuleChain.outerRule(HiltAndroidRule(this)).
        around(SettingsScreenTestRule(...))

  // UI tests here.
}

Usa un punto de entrada antes de que el componente singleton esté disponible

La anotación @EarlyEntryPoint proporciona una salida de escape cuando se debe crear un punto de entrada de Hilt antes de que el componente singleton esté disponible en una prueba de Hilt.

Obtén más información sobre @EarlyEntryPoint en la documentación de Hilt.

Recursos adicionales

Para obtener más información sobre las pruebas, consulta los siguientes recursos adicionales:

Documentación

Mira contenido