Pruebas en Kotlin: unit, integration, e2e de la A a la Z
\n\nEl desarrollo moderno en Kotlin requiere un enfoque sistemático para las pruebas. Independientemente de si escribes un backend con Ktor o una aplicación móvil para Android, una cobertura de código de calidad con pruebas es la clave para la estabilidad y previsibilidad del proyecto. En este artículo, analizaremos tres niveles de pruebas: unitarias (unit), de integración (integration) y de extremo a extremo (e2e). Aprenderás qué herramientas usar, cómo escribir pruebas efectivas en Kotlin y evitar errores típicos.
\n\n1. Pruebas unitarias en Kotlin: la base fundamental
\n\nLas pruebas unitarias verifican el funcionamiento de funciones, clases o métodos individuales de forma aislada de las dependencias externas. En Kotlin, para esto se utiliza principalmente el framework JUnit 5 junto con la biblioteca de mocks MockK.
\n\nConfiguración del proyecto
\n\nAñade las dependencias en build.gradle.kts:
dependencies {\n testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")\n testImplementation("io.mockk:mockk:1.13.8")\n}\n\n\nEjemplo de prueba unitaria
\n\nSupongamos que tenemos un servicio simple para calcular un descuento:
\n\nclass DiscountService {\n fun applyDiscount(price: Double, discountPercent: Int): Double {\n require(discountPercent in 0..100) { "El porcentaje de descuento debe estar entre 0 y 100" }\n return price * (100 - discountPercent) / 100\n }\n}\n\n\nPrueba para este servicio:
\n\nimport org.junit.jupiter.api.Assertions.*\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.assertThrows\n\nclass DiscountServiceTest {\n\n private val service = DiscountService()\n\n @Test\n fun `applyDiscount calcula correctamente un descuento del 50 por ciento`() {\n val result = service.applyDiscount(1000.0, 50)\n assertEquals(500.0, result, 0.001)\n }\n\n @Test\n fun `applyDiscount lanza una excepción cuando el porcentaje es inválido`() {\n assertThrows<IllegalArgumentException> {\n service.applyDiscount(100.0, 150)\n }\n }\n}\n\n\nConsejo: Usa assertThrows para verificar excepciones y nombra las pruebas con cadenas claras; esto mejora la legibilidad de los informes.
Mocking con MockK
\n\nCuando tu clase depende de otros componentes (por ejemplo, un repositorio o una API), usa mocks:
\n\nclass OrderService(private val paymentGateway: PaymentGateway) {\n fun processOrder(amount: Double): String {\n if (paymentGateway.charge(amount)) {\n return "Pago exitoso"\n }\n return "Error de pago"\n }\n}\n\n// Test\n@Test\nfun `processOrder devuelve éxito cuando el pago es exitoso`() {\n val mockGateway = mockk<PaymentGateway>()\n every { mockGateway.charge(any()) } returns true\n\n val service = OrderService(mockGateway)\n val result = service.processOrder(500.0)\n\n assertEquals("Pago exitoso", result)\n verify { mockGateway.charge(500.0) }\n}\n\n\n2. Pruebas de integración: verificación de la interacción entre componentes
\n\nLas pruebas de integración verifican cómo diferentes partes del sistema funcionan juntas: base de datos, APIs externas, sistema de archivos. Para Kotlin, Testcontainers es ideal: una biblioteca para levantar contenedores Docker durante las pruebas.
\n\nEjemplo con base de datos PostgreSQL
\n\nAñade las dependencias:
\n\ntestImplementation("org.testcontainers:testcontainers:1.19.3")\ntestImplementation("org.testcontainers:postgresql:1.19.3")\n\n\nEscribamos una prueba para la capa DAO:
\n\nclass UserRepositoryIntegrationTest {\n\n companion object {\n private val postgres = PostgreSQLContainer<Nothing>("postgres:15").apply {\n withDatabaseName("testdb")\n withUsername("test")\n withPassword("test")\n start()\n