Introducción: ¿Por qué las pruebas sin mocks son como café sin cafeína?
\n\n¡Hola, colega! Si escribes en Kotlin y quieres que tu código no se rompa en cada segundo commit, tarde o temprano llegarás a las pruebas. Y en las pruebas, a los objetos mock y fixtures. Sin ellos, las pruebas unitarias se convierten en pruebas de integración: no pruebas una sola clase, sino toda una cadena de dependencias. Y si la base de datos falla o la API devuelve un 500, la prueba fallará, aunque tu código pueda ser perfecto.
\n\nEn este artículo, analizaremos cómo usar correctamente objetos Mock y fixtures en pruebas de Kotlin para que las pruebas sean rápidas, confiables y te digan la verdad, no "todo está roto, arréglalo". ¡Allá vamos!
\n\n¿Qué es un mock y para qué sirve en Kotlin?
\n\nUn objeto Mock es una simulación de un objeto real. En lugar de levantar un servidor completo o una base de datos, creamos un "simulacro" que se comporta como le indiquemos. Esto permite aislar el código bajo prueba del mundo exterior.
\n\nConceptos principales
\n- \n
- Mock — un objeto cuyo comportamiento de métodos controlamos. Podemos definir qué devolverá y verificar si fue llamado. \n
- Stub — un caso particular de mock que solo devuelve valores predefinidos. No verifica llamadas. \n
- Fixture — un conjunto de datos o estados preparados previamente, necesarios para ejecutar la prueba. Por ejemplo, un usuario de prueba o configuraciones de BD. \n
En el mundo de Kotlin hay dos actores principales: MockK (nativo de Kotlin) y Mockito (popular en Java). Usaremos MockK, ya que se lleva mejor con corrutinas, funciones de extensión y clases selladas.
\n\nConfiguración del proyecto: agregando MockK y JUnit 5
\n\nPara empezar, agreguemos las dependencias en build.gradle.kts:
dependencies {\n testImplementation("io.mockk:mockk:1.13.8")\n testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")\n}\n\n\nAhora podemos escribir pruebas. Crearemos un servicio simple y lo probaremos usando mocks.
\n\nPrimer ejemplo: simulando un repositorio
\n\nImagina que tenemos un UserService que depende de UserRepository:
class UserService(private val repository: UserRepository) {\n fun getUserName(id: Long): String {\n val user = repository.findById(id)\n ?: throw IllegalArgumentException("User not found")\n return user.name\n }\n}\n\n\nAhora escribamos una prueba usando un objeto mock para el repositorio:
\n\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.verify\nimport org.junit.jupiter.api.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFailsWith\n\nclass UserServiceTest {\n\n private val repository = mockk<UserRepository>()\n private val service = UserService(repository)\n\n @Test\n fun `should return user name when user exists`() {\n // Arrange\n every { repository.findById(1L) } returns User(1L, "Alice")\n\n // Act\n val name = service.getUserName(1L)\n\n // Assert\n assertEquals("Alice", name)\n verify { repository.findById(1L) }\n }\n\n @Test\n fun `should throw exception when user not found`() {\n // Arrange\n every { repository.findById(any()) } returns null\n\n // Act & Assert\n assertFailsWith<IllegalArgumentException> {\n service.getUserName(99L)\n }\n }\n}\n\n\nObserva: every define el comportamiento del mock, verify comprueba que el método fue llamado. Esta es la base de objetos Mock y fixtures en pruebas de Kotlin.
Fixtures: preparando datos con antelación
\n\nCuando hay muchas pruebas, duplicar la creación de datos es una mala práctica. Aquí es donde entran las fixtures. En Kotlin, es conveniente