Estructurar pruebas con JUnit 5 y Kotlin
JUnit 5 mas los nombres con backticks de Kotlin convierte las pruebas en especificaciones legibles y autodocumentadas.
JUnit 5 (tambien llamado JUnit Jupiter) es el motor de pruebas por defecto en Spring Boot 3 y la opcion estandar para proyectos Kotlin. Una prueba es simplemente una funcion anotada con @Test, declarada dentro de una clase normal. A diferencia de JUnit 4, ni la clase ni los metodos de prueba necesitan ser public, lo que encaja perfecto con Kotlin ya que los miembros son public por defecto y el framework puede usar reflexion para acceder a ellos. Lo incorporas con la dependencia org.junit.jupiter:junit-jupiter, o gratis mediante spring-boot-starter-test, y lo ejecutas con la tarea test de Gradle.
La caracteristica mas amigable con Kotlin es el identificador con backticks. Kotlin permite nombrar una funcion con cualquier cadena envolviendola en backticks, asi que en lugar de un nombre apretado en camelCase como addsTwoNumbers escribes fun `adds two numbers`(). El reporte de pruebas se lee entonces como una frase en lenguaje natural, lo que hace que los fallos sean comprensibles al instante. Esta es la forma idiomatica de nombrar pruebas en Kotlin y combina naturalmente con la estructura Arrange-Act-Assert dentro del cuerpo. Ten en cuenta que los nombres con espacios no son legales en el target de plataforma JVM para pruebas instrumentadas de Android, pero funcionan en todas partes en la JVM estandar donde corre Spring.
Las anotaciones de ciclo de vida controlan la preparacion y la limpieza. Un metodo anotado con @BeforeEach se ejecuta antes de cada metodo de prueba, dando a cada prueba un fixture fresco y aislado; @AfterEach se ejecuta despues de cada una para liberar recursos. Tambien existen @BeforeAll y @AfterAll para preparacion unica compartida en toda la clase, pero como JUnit 5 crea una nueva instancia de prueba por metodo por defecto, esos deben ser estaticos, lo que en Kotlin significa colocarlos dentro de un companion object anotado con @JvmStatic (o cambiar la clase al ciclo de vida PER_CLASS). Prefiere @BeforeEach para todo lo que deba ser independiente entre pruebas, para evitar estado mutable compartido y errores de orden inestables.
Las aserciones son el corazon de una prueba. JUnit 5 las ofrece como funciones de nivel superior en org.junit.jupiter.api.Assertions, y las variantes con sabor Kotlin del mismo paquete se leen limpiamente. assertEquals(expected, actual) verifica un valor, con el argumento esperado primero por convencion. assertThrows<T> { ... } es la forma idiomatica en Kotlin: es una funcion inline reificada que ejecuta el lambda, afirma que se lanzo una excepcion del tipo T y devuelve la excepcion capturada para que puedas inspeccionar su mensaje. Esto es mucho mejor que envolver codigo en try/catch con una llamada manual a fail().
Cuando necesitas verificar varias propiedades independientes de un resultado, usa assertAll. Recibe un vararg de lambdas ejecutables y los corre todos aunque algunos fallen, y luego reporta cada fallo junto. Esto evita el ciclo frustrante de arreglar una asercion, volver a ejecutar y descubrir la siguiente rota. Agrupa verificaciones relacionadas (por ejemplo, todos los campos de un objeto devuelto) dentro de un solo bloque assertAll para que una sola ejecucion muestre el panorama completo de lo que esta mal.
@DisplayName proporciona una etiqueta legible para una prueba o una clase de prueba completa, mostrada en los runners del IDE y en los reportes de CI. Con los nombres con backticks de Kotlin muchas veces no lo necesitas en metodos individuales, pero brilla en la clase misma y cuando quieres emoji, puntuacion o redaccion que ni los backticks expresan con limpieza. Tambien puedes aplicar @DisplayNameGeneration con un generador como ReplaceUnderscores para convertir nombres de metodos automaticamente, aunque los backticks suelen hacerlo innecesario.
Juntando todo, una clase de prueba JUnit 5 bien estructurada en Kotlin se lee de arriba a abajo como una especificacion: un @DisplayName que describe la unidad bajo prueba, un @BeforeEach que construye un sujeto fresco y una serie de funciones con nombre en backticks, cada una siguiendo Arrange-Act-Assert con aserciones enfocadas. Manten un comportamiento logico por prueba, nombrala segun el comportamiento en vez del metodo que llama, y deja que assertAll agrupe las verificaciones relacionadas. Este estilo escala desde una calculadora diminuta hasta una prueba completa de servicio Spring sin perder claridad.
Para Spring Boot en concreto, estas mismas primitivas sustentan todo lo que escribiras despues: un @SpringBootTest o un @WebMvcTest segmentado sigue siendo solo una clase JUnit 5 con metodos @Test, hooks de ciclo de vida y estas aserciones. Dominar primero la estructura plana de JUnit significa que cuando agregues el contexto de prueba de Spring, MockMvc o @MockkBean, solo estaras superponiendo cableado del framework sobre fundamentos que ya entiendes. Ejecuta tu suite a menudo con ./gradlew test y trata la barra verde como una precondicion para cada commit.
import org.junit.jupiter.api.BeforeEachimport org.junit.jupiter.api.DisplayNameimport org.junit.jupiter.api.Testimport org.junit.jupiter.api.assertThrowsimport org.junit.jupiter.api.Assertions.assertEqualsclass Calculator {fun add(a: Int, b: Int): Int = a + bfun divide(a: Int, b: Int): Int {require(b != 0) { "cannot divide by zero" }return a / b}}@DisplayName("Calculator")class CalculatorTest {private lateinit var calculator: Calculator@BeforeEachfun setUp() {// Runs before every test, so each gets a fresh instancecalculator = Calculator()}@Testfun `adds two numbers`() {val result = calculator.add(2, 3)assertEquals(5, result) // expected first, actual second}@Testfun `throws when dividing by zero`() {val ex = assertThrows<IllegalArgumentException> {calculator.divide(10, 0)}assertEquals("cannot divide by zero", ex.message)}}
A complete, idiomatic JUnit 5 test class in Kotlin: @DisplayName on the class, @BeforeEach for a fresh fixture, backtick test names, and assertEquals / assertThrows<T>. This runs on plain Kotlin stdlib plus the JUnit 5 dependency.
import org.junit.jupiter.api.Testimport org.junit.jupiter.api.assertAllimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Assertions.assertTruedata class User(val name: String, val age: Int, val active: Boolean)class UserTest {@Testfun `builds a valid adult user`() {val user = User(name = "Ada", age = 36, active = true)// All three checks run; a single failure does not hide the othersassertAll({ assertEquals("Ada", user.name) },{ assertTrue(user.age >= 18, "user should be an adult") },{ assertTrue(user.active, "user should be active") },)}}
assertAll runs every assertion even if earlier ones fail, then reports all failures at once. Ideal for checking multiple fields of a single returned object. Requires the JUnit 5 dependency.
import org.junit.jupiter.api.AfterAllimport org.junit.jupiter.api.BeforeAllimport org.junit.jupiter.api.Testclass DatabaseSuiteTest {companion object {@JvmStatic@BeforeAllfun startSharedResource() {// e.g. spin up a Testcontainer once for the whole classprintln("shared setup")}@JvmStatic@AfterAllfun stopSharedResource() {println("shared teardown")}}@Testfun `uses the shared resource`() {// ...}}
@BeforeAll / @AfterAll run once per class. Because JUnit 5 uses a new instance per test by default, in Kotlin they must live in a companion object with @JvmStatic.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. In a Kotlin JUnit 5 test class, why is assertThrows<IllegalArgumentException> { service.call() } preferred over a manual try/catch with fail()?