Por qué no se testea — las razones reales

La mayoría de los devs Android saben que deberían escribir tests. La mayoría no los escribe. Las razones que se dicen en voz alta: "no hay tiempo", "el proyecto es chico", "después lo agrego". La razón real detrás de todas esas: el código no fue escrito para ser testeable.

Una Activity que hace llamadas de red directamente, un ViewModel que accede a SharedPreferences, una clase que instancia sus propias dependencias — ese código es difícil de testear no porque testear sea difícil, sino porque la arquitectura no lo permite. Cuando el código está bien estructurado (repositorios, inyección de dependencias, separación de capas), escribir el test es relativamente simple.

Los tests no se agregan despuésSi esperás a que el código "esté listo" para agregar tests, ese momento nunca llega. El código que es difícil de testear al principio se vuelve imposible de testear cuando tiene seis meses y diez features encima. La testabilidad se diseña desde el principio, no se agrega después.

El costo real de no tener tests

No tener tests no es gratis. El costo se paga de otras formas:

  • Regresiones en producción: cada vez que cambiás algo, podés romper otra cosa sin darte cuenta. Sin tests, tu única red de seguridad es QA manual — que es lento, costoso y nunca completo.
  • Miedo a refactorizar: cuando el código está acoplado y no tiene tests, nadie quiere tocarlo. El código se pudre lentamente mientras todos evitan el "código legacy" que nadie entiende.
  • Debugging más lento: encontrar un bug sin tests es reproducirlo manualmente, paso a paso, en el dispositivo. Con un test de regresión, el bug queda capturado y verificado en segundos.
  • Onboarding más difícil: los tests son documentación ejecutable. Un dev nuevo que lee los tests de un repositorio entiende exactamente qué se supone que hace ese código.

La pirámide de tests

No todos los tests son iguales en velocidad, costo y valor. La pirámide describe la distribución ideal:

#           /▲\
#          / UI \        ← Pocos (lentos, frágiles, costosos)
#         /  tests\        Espresso, Compose UI test
#        /──────────\
#       / Integration \  ← Algunos
#      /    tests      \   Room + Repository, API + Retrofit
#     /──────────────────\
#    /    Unit tests      \ ← Muchos (rápidos, baratos, precisos)
#   /  JUnit + Mockk/Fake  \  ViewModels, Use Cases, lógica pura
#  /────────────────────────\

# Regla práctica:
# 70% unit tests — rápidos, sin Android, corren en la JVM
# 20% integration tests — necesitan contexto Android o dependencias reales
# 10% UI tests — el flujo completo end-to-end

El error más común: invertir la pirámide. Muchos UI tests y pocos unit tests. Los UI tests son valiosos pero lentos y frágiles — un cambio de layout puede romper diez tests sin que haya ningún bug real. Los unit tests son rápidos, precisos y resistentes a cambios de UI.

Qué vale la pena testear

  • Lógica de negocio en Use Cases: si tenés un CalcularPrecioConDescuentoUseCase, ese cálculo debe tener tests. Es el código más valioso y el que más importa que sea correcto.
  • ViewModels: los estados que emite ante diferentes acciones y respuestas del repositorio. Son el corazón de la capa de presentación.
  • Repositorios: que combinan correctamente la fuente local y remota, que el caché funciona, que los errores se propagan bien.
  • DAOs de Room: que las queries retornan los datos correctos, que los filtros funcionan, que las relaciones se resuelven bien.
  • Código con condiciones complejas: cualquier función con múltiples if, when o casos límite.
  • Bugs corregidos: cada vez que arreglás un bug, escribí un test de regresión que lo reproduzca. Así el bug nunca vuelve.

Qué NO vale la pena testear

  • Getters y setters triviales: fun getNombre() = nombre no necesita test.
  • Código generado: Room genera los DAOs, Hilt genera factories. No testees código que no escribiste.
  • UI layout y diseño: si el botón está 8dp a la izquierda en lugar de 10dp, eso no es un test unitario — es revisión visual.
  • Código que es 100% delegación a librerías: si tu repositorio solo llama a Retrofit sin ninguna lógica propia, el test no agrega valor — estás testeando Retrofit.
  • Android framework directamente: no hagas unit tests de Activities o Fragments — son difíciles de instanciar, lentos y frágiles. Testear el ViewModel que usa la Activity es suficiente.

Setup inicial — las dependencias

// build.gradle (app)
dependencies {
    // Unit tests — corren en la JVM, sin Android
    testImplementation("junit:junit:4.13.2")
    testImplementation("io.mockk:mockk:1.13.10")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
    testImplementation("app.cash.turbine:turbine:1.1.0")  // testear Flows
    testImplementation("com.google.truth:truth:1.4.2")    // assertions legibles

    // Tests instrumentados — necesitan dispositivo/emulador
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation("androidx.test:runner:1.5.2")
    androidTestImplementation("androidx.test:rules:1.5.0")

    // Room testing
    androidTestImplementation("androidx.room:room-testing:2.6.1")

    // Hilt testing
    androidTestImplementation("com.google.dagger:hilt-android-testing:2.51")
    kspAndroidTest("com.google.dagger:hilt-compiler:2.51")
}
# Estructura de carpetas:
# app/src/
# ├── main/           ← código de producción
# ├── test/           ← unit tests (JVM, rápidos)
# │   └── java/ar/pensa/miapp/
# │       ├── viewmodel/
# │       ├── usecase/
# │       └── repository/
# └── androidTest/    ← tests instrumentados (dispositivo, lentos)
#     └── java/ar/pensa/miapp/
#         ├── database/
#         └── ui/

# Correr unit tests:
./gradlew test

# Correr tests instrumentados (necesita emulador/dispositivo):
./gradlew connectedAndroidTest