12 - Pruebas de integración: Testcontainers
Las unitarias (tema 05) sustituyen las dependencias por dobles: comprueban la lógica de cada pieza aislada, pero no la infraestructura. Un mock no ejecuta SQL, no tiene un esquema y no aplica restricciones. Cuando lo que hay que comprobar es la colaboración con una dependencia real — el caso típico: el DAO contra la base de datos — la pieza es la prueba de integración.
1. Qué se prueba
La prueba de integración del DAO ejecuta el código real contra un motor real:
| Se comprueba | No se comprueba |
|---|---|
| El SQL correcto contra el motor real | Reglas de negocio (pruebas de dominio) |
Restricciones: claves, NOT NULL, únicas | La interfaz y los flujos de usuario (E2E, tema 10) |
| El mapeo de resultados a modelo | El rendimiento |
| Unitarias (Mockito) | Integración (BD real) |
|---|---|
| Milisegundos | Segundos |
| Muchas: la base de la pirámide | Pocas: lo que las unitarias no pueden ver |
2. Testcontainers
Testcontainers levanta en Docker las dependencias reales de la aplicación durante las pruebas: el contenedor se crea al empezar la suite, se usa y se destruye al terminar. No hay una base de datos compartida que ensuciar: cada ejecución parte de cero.
La clave es usar la misma versión de MariaDB que producción y apuntar el código bajo prueba a las credenciales del contenedor:
@Testcontainers
class BookDaoIntegrationTest {
@Container
static MariaDBContainer<?> mariadb = new MariaDBContainer<>("mariadb:12.3");
static BookDao bookDao;
@BeforeAll
static void startDatabase() {
// 1. crea el esquema: las migraciones del proyecto
Flyway.configure()
.dataSource(mariadb.getJdbcUrl(),
mariadb.getUsername(),
mariadb.getPassword())
.load()
.migrate();
// 2. código bajo prueba, conectado al contenedor
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(mariadb.getJdbcUrl());
dataSource.setUsername(mariadb.getUsername());
dataSource.setPassword(mariadb.getPassword());
bookDao = new JdbcBookDao(dataSource);
}
}
| Elemento | Función |
|---|---|
@Testcontainers | Activa la gestión de contenedores en la suite |
@Container | Levanta y destruye el contenedor con la suite |
getJdbcUrl() y similares | Credenciales del contenedor, sin valores fijados a mano |
Flyway…migrate() | Aplica las migraciones: esquema idéntico al del proyecto |
El DAO y la migración del esquema se explican en el módulo de Bases de datos; aquí lo nuevo es el entorno real y efímero donde se comprueba.
3. La prueba
Con el contenedor y el esquema listos, la prueba es AAA convencional:
@Test
void savesAndFindsBook() {
// Arrange
Book book = new Book(0, "Clean Code", new BigDecimal("34.50"), 1);
// Act
Book saved = bookDao.save(book);
// Assert
Optional<Book> found = bookDao.findById(saved.id());
assertTrue(found.isPresent());
assertEquals("Clean Code", found.get().title());
}
Aquí no hay nada simulado: el id lo genera el AUTO_INCREMENT de la tabla, la clave foránea comprueba que la editorial existe y el mapeo usa el esquema real. Si el SQL o el mapeo están mal, la prueba lo detecta.
4. Datos de prueba
Cada prueba prepara los datos que consulta y no asume lo que otras dejaron (pruebas aisladas, tema 04). Lo más sencillo: cada una inserta lo que necesita y trabaja con los id que la propia base devuelve, como el saved.id() del ejemplo.
5. Con Spring Boot
Cuando la aplicación es Spring Boot (módulo de Bases de datos), la anotación @ServiceConnection sobre el @Container conecta la aplicación de prueba al contenedor sin configurar la DataSource a mano.