# Regla: Testing en Kotlin

Esta regla es OBLIGATORIA para todo código Kotlin nuevo o modificado.
Las coroutines y los flows requieren herramientas específicas para testearse
correctamente. Un test de coroutine sin `runTest` no es determinista.

---

## Stack de testing obligatorio

| Herramienta | Propósito |
|------------|----------|
| JUnit 5 | Framework base de tests |
| MockK | Mocking idiomático para Kotlin (no Mockito) |
| Turbine | Testing de `Flow` con `test {}` API |
| kotlinx-coroutines-test | `runTest`, `TestDispatcher`, `advanceUntilIdle` |
| Kover | Cobertura de código (reemplaza JaCoCo para Kotlin) |
| Compose UI Test | Testing de Composables con `createComposeRule` |

```kotlin
// build.gradle.kts — dependencias de test
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
    testImplementation("io.mockk:mockk:1.13.10")
    testImplementation("app.cash.turbine:turbine:1.1.0")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
```

---

## runTest y TestDispatcher para coroutines

`runTest` es el único punto de entrada correcto para tests de suspend functions.
Reemplaza `runBlocking` en tests — maneja los virtual timers del test framework.

```kotlin
// MAL — runBlocking no controla el tiempo virtual ni los dispatchers de coroutines
@Test
fun `obtener usuario por id retorna usuario correcto`() = runBlocking {
    val resultado = repositorio.obtenerPorId(uuid)
    assertEquals("Juan", resultado?.nombre)
}

// BIEN — runTest con dispatcher controlado
@Test
fun `obtener usuario por id retorna usuario correcto`() = runTest {
    val resultado = repositorio.obtenerPorId(uuid)
    assertEquals("Juan", resultado?.nombre)
}

// BIEN — StandardTestDispatcher para controlar avance del tiempo
@Test
fun `cargando pedidos emite loading luego success`() = runTest {
    val dispatcher = StandardTestDispatcher(testScheduler)
    val viewModel = PedidoViewModel(obtenerPedidosUseCase, dispatcher)

    val estados = mutableListOf<UiState<*>>()
    val job = launch { viewModel.uiState.collect { estados.add(it) } }

    advanceUntilIdle()

    assertEquals(UiState.Loading, estados.first())
    assertIs<UiState.Success<*>>(estados.last())
    job.cancel()
}
```

---

## Turbine para testing de Flow

```kotlin
// MAL — collect en test puede colgarse o perder emisiones
@Test
fun `flujo emite pedidos`() = runTest {
    val emisiones = mutableListOf<List<Pedido>>()
    val job = launch { repositorio.obtenerTodos().collect { emisiones.add(it) } }
    job.cancel()
    assertTrue(emisiones.isNotEmpty())
}

// BIEN — Turbine provee API declarativa y determinista
@Test
fun `flujo emite lista de pedidos`() = runTest {
    repositorio.obtenerTodos().test {
        val pedidos = awaitItem()
        assertEquals(3, pedidos.size)
        cancelAndIgnoreRemainingEvents()
    }
}

// BIEN — verificar múltiples emisiones en orden
@Test
fun `estado transiciona de loading a success`() = runTest {
    viewModel.uiState.test {
        assertIs<UiState.Loading>(awaitItem())
        val success = awaitItem()
        assertIs<UiState.Success<*>>(success)
        cancelAndIgnoreRemainingEvents()
    }
}
```

---

## MockK para mocks idiomáticos en Kotlin

```kotlin
// Configuración típica con MockK
@ExtendWith(MockKExtension::class)
class CrearPedidoUseCaseTest {

    @MockK
    private lateinit var pedidoRepo: PedidoRepository

    @MockK
    private lateinit var inventarioRepo: InventarioRepository

    private lateinit var crearPedido: CrearPedidoUseCase

    @BeforeEach
    fun setup() {
        crearPedido = CrearPedidoUseCase(pedidoRepo, inventarioRepo)
    }

    @Test
    fun `crear pedido con items disponibles retorna pedido creado`() = runTest {
        // Given
        val items = listOf(ItemPedido(productoId = uuid, cantidad = 2))
        coEvery { inventarioRepo.verificarDisponibilidad(items) } returns true
        coJustRun { pedidoRepo.guardar(any()) }

        // When
        val resultado = crearPedido(items)

        // Then
        assertTrue(resultado.isSuccess)
        coVerify(exactly = 1) { pedidoRepo.guardar(any()) }
    }
}
```

---

## Patrón Given-When-Then

Todos los tests siguen el patrón Given-When-Then con comentarios explícitos.

```kotlin
@Test
fun `crear pedido sin items retorna error de validación`() = runTest {
    // Given
    val itemsVacios = emptyList<ItemPedido>()

    // When
    val resultado = crearPedido(itemsVacios)

    // Then
    assertTrue(resultado.isFailure)
    assertIs<IllegalArgumentException>(resultado.exceptionOrNull())
}
```

---

## Cobertura con Kover

```kotlin
// build.gradle.kts
plugins {
    id("org.jetbrains.kotlinx.kover") version "0.7.6"
}

koverReport {
    filters {
        excludes {
            classes("**/di/**", "**/*Module*", "**/*_Factory*")
        }
    }
    verify {
        rule {
            bound { minValue = 80 }
        }
    }
}
```

---

## Testing de Composables

```kotlin
class PantallaPedidosTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun `muestra lista de pedidos cuando estado es success`() {
        // Given
        val pedidos = listOf(Pedido(id = uuid, total = 150.0))
        val estado = UiState.Success(pedidos)

        // When
        composeTestRule.setContent {
            PantallaPedidos(uiState = estado)
        }

        // Then
        composeTestRule.onNodeWithText("$150.00").assertIsDisplayed()
    }

    @Test
    fun `muestra indicador de carga cuando estado es loading`() {
        composeTestRule.setContent {
            PantallaPedidos(uiState = UiState.Loading)
        }
        composeTestRule.onNodeWithTag("loading_indicator").assertIsDisplayed()
    }
}
```

---

## Checklist de testing Kotlin antes de abrir PR

- [ ] Tests de coroutines usan `runTest`, no `runBlocking`
- [ ] Tests de Flow usan Turbine (`flow.test { }`)
- [ ] Mocks creados con MockK, no con Mockito
- [ ] `coEvery`/`coVerify` para suspend functions mockeadas
- [ ] Patrón Given-When-Then con comentarios explícitos
- [ ] Cobertura >= 80% verificada con Kover
- [ ] Tests de Composables con `createComposeRule`
- [ ] Sin `Thread.sleep()` ni delays fijos en tests
