Tabla de Contenidos

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.

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.