Saltar al contenido principal

Frameworks de testing

Hacer testing requiere dos cosas trabajando juntas:

  1. Un Framework de Testing. Es el conductor. Te dice a tus tests cuándo correr, cómo organizarlos, y reporta los resultados. Pensalo como el test runner.
  2. Un Framework de Mocking. Es el suplente. Te permite reemplazar dependencias reales (bases de datos, servicios externos) con versiones fake que controlás.

La solución out-of-the-box de Spring Boot

Spring Boot te da una base sólida con spring-boot-starter-webmvc-test (spring-boot-starter-test si todavía estás en Spring Boot 3).

Esta única dependencia trae todo lo que necesitás:

FrameworkPropósitoPor Qué Importa
JUnit 5Test runnerEl estándar de facto para testing en Java. Corre tus tests, maneja el ciclo de vida (before/after), se integra con IDEs y pipelines de CI.
MockitoMockingCrea objetos fake que se comportan como los reales. Esencial para aislar el código que realmente estás testeando.
AssertJAsercionesAserciones "fluent" que se leen como inglés. assertThat(x).isEqualTo(y) le gana a assertEquals(x, y) cualquier día.
HamcrestMatchersOtra librería de aserciones con matchers expresivos. AssertJ es generalmente preferido.
java/com/example/service/FilmServiceTest.java
@SpringBootTest
class FilmServiceTest {

@Autowired
private FilmService filmService;

@MockBean
private FilmRepository filmRepository;

@Test
void whenFilmExists_thenReturnFilm() {
Film mockFilm = new Film("ACADEMY DINOSAUR", "A Epic Drama of a Feminist Pilot who must battle a Dentist in Australia", 2006);
when(filmRepository.findById(1L)).thenReturn(Optional.of(mockFilm));

Film result = filmService.getFilmById(1L);

assertThat(result.getTitle()).isEqualTo("ACADEMY DINOSAUR");
assertThat(result.getDescription()).contains("Drama");
}

@Test
void whenFilmNotExists_thenThrowException() {
when(filmRepository.findById(999L)).thenReturn(Optional.empty());

assertThatThrownBy(() -> filmService.getFilmById(999L))
.isInstanceOf(FilmNotFoundException.class);
}
}

Este es tu pan y manteca. @SpringBootTest levanta el contexto completo de la aplicación (útil para tests de integración), @MockBean crea un mock de Mockito y lo inyecta donde se necesita, y @Test de JUnit marca el método como un test.

Fijate la cadena de AssertJ: assertThat(result.getTitle()).isEqualTo("ACADEMY DINOSAUR"). Se lee casi como una oración, lo cual hace que el debugging sea mucho menos doloroso cuando estás mirando un test fallando a las 2 AM.

MockK para Kotlin

Mockito fue diseñado para Java. Las características del lenguaje de Kotlin crean varios puntos de fricción que Mockito simplemente no fue construido para manejar.

  • Final Classes by Default: En Kotlin, todas las clases son final por defecto. Mockito (históricamente) no podía mockear clases finales sin plugins y workarounds.
  • Coroutines: Si estás usando coroutines de Kotlin (funciones suspend), Mockito no tiene idea de qué hacer con ellas. Las trata como métodos regulares, lo que significa que tu código async de tests se vuelve síncrono por accidente.
  • La palabra clave when: when es una palabra reservada en Kotlin.
  • Object/Static Mocking: Las declaraciones object de Kotlin son singletons. Mockearlos en Mockito es doloroso.

MockK fue creado específicamente para Kotlin, y se nota en cada decisión de API:

kotlin/com/example/service/FilmServiceTest.kt
@ExtendWith(MockKExtension::class)
class FilmServiceTest {

@InjectMockKs
private lateinit var filmService: FilmService

@MockK
private lateinit var filmRepository: FilmRepository

@Test
fun `when film exists, return film`() = every { filmRepository.findById(1L) } returns Film("ACADEMY DINOSAUR", "A Epic Drama", 2006)

val result = filmService.getFilmById(1L)

assertThat(result.title).isEqualTo("ACADEMY DINOSAUR")
assertThat(result.description).contains("Drama")
}

@Test
fun `when film not exists, throw exception`() = every { filmRepository.findById(999L) } returns null {
assertThrows<FilmNotFoundException> { filmService.getFilmById(999L) }
}
}

Limpio, legible, y sin backticks necesarios. La palabra clave every reemplaza a when, y todo se siente nativo de Kotlin.

Spock para Groovy

Si estás abierto a escribir tus tests en Groovy, Spock es un cambio de juego. Spock no solo reemplaza a Mockito, también reemplaza a JUnit. Es un framework de testing completo que incluye:

  1. Test Runner. Como JUnit, pero con superpoderes.
  2. Mocking Engine. Como Mockito, pero incluido en el lenguaje.
  3. Assertion Engine. Diferente a cualquier cosa que hayas visto en librerías Java.
  4. Estructura BDD. Te guía a escribir tests como historias.
groovy/com/example/service/FilmServiceSpec.groovy
@SpringBootTest
class FilmServiceSpec extends Specification {

FilmService filmService
FilmRepository filmRepository = Mock()

def setup() {
filmService = new FilmService(filmRepository)
}

def "when film exists, return film"() {
given: "a mock film"
Film mockFilm = new Film("ACADEMY DINOSAUR", "A Epic Drama of a Feminist Pilot", 2006)

and: "the repository returns the film"
1 * filmRepository.findById(1L) >> Optional.of(mockFilm)

when: "we request the film"
Film result = filmService.getFilmById(1L)

then: "we get the expected film"
result.title == "ACADEMY DINOSAUR"
result.releaseYear == 2006
}

def "when film not exists, throw exception"() {
given: "no film in repository"
1 * filmRepository.findById(999L) >> Optional.empty()

when: "we request a non-existent film"
filmService.getFilmById(999L)

then: "we get an exception"
thrown(FilmNotFoundException)
}
}

Mirá esa estructura: given:, when:, then:. Te fuerza a organizar tus tests en una narrativa. Dado cierto setup, cuando una acción sucede, entonces estos son los resultados esperados. Tu yo del futuro (o el pobre desarrollador que herede tu código) te lo va a agradecer.

La sintaxis de mocking es refrescantemente simple también:

1 * filmRepository.findById(1L) >> Optional.of(mockFilm)

Esto se lee como: "Esperá exactamente 1 llamada a findById(1L), y devolvé esta película mock." Sin boilerplate, sin verbosidad.

¿Cuál elegir?

No hay una elección incorrecta, pero hay mejores elecciones para tu contexto.

Scroll to zoom • Drag corner to resize

El mejor test es aquel que escribís. Elegí la herramienta que te haga querer escribir tests, y vas a terminar con un mejor test suite que si te hubieras forzado a usar la herramienta "correcta".