====== 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 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.
Testcontainers necesita **Docker** disponible donde se ejecutan las pruebas: en el equipo local y en el sistema de CI.
Son más lentas que las unitarias y no las sustituyen: la pirámide del tema 04 sigue mandando. Las unitarias llevan el peso; unas pocas de integración comprueban la fricción real con la infraestructura.