Café y Código

10. Desarrollo de tests con base de datos

El problema: MySQL en producción, ¿y en los tests?

En un sistema real suele usarse MySQL (u otro motor). Los tests no deberían depender de que alguien haya “levantado MySQL a mano” antes de pulsar Run: eso es frágil y rompe la CI.

La propuesta habitual es declarar en el propio test qué base necesitas y crearla o conectarla al inicio de la clase de test (por ejemplo con @BeforeAll). Así el contrato queda claro: antes de ejecutar cualquier @Test, el esquema existe y está en un estado conocido.

Opción práctica: H2 en memoria (tests rápidos)

H2 es una base embebida que puede ejecutarse en memoria. No sustituye por completo a MySQL (el SQL puede diferir), pero es ideal para probar DAOs y JDBC sin Docker.

En @BeforeAll abres la URL JDBC, ejecutas tu script .sql (o migraciones) y recién entonces construyes el objeto que usa Connection (por ejemplo un UsuarioDAO).

Dependencias Maven (solo para test)

JUnit 5, AssertJ (assertions legibles) y H2 suelen declararse con <scope>test</scope> para no mezclarlas con el código de producción.

pom.xml
XML
1 <!-- pom.xml (fragmento) -->
2 <dependencies>
3 <dependency>
4 <groupId>org.junit.jupiter</groupId>
5 <artifactId>junit-jupiter-api</artifactId>
6 <version>5.10.0</version>
7 <scope>test</scope>
8 </dependency>
9 <dependency>
10 <groupId>org.assertj</groupId>
11 <artifactId>assertj-core</artifactId>
12 <version>3.24.2</version>
13 <scope>test</scope>
14 </dependency>
15 <dependency>
16 <groupId>com.h2database</groupId>
17 <artifactId>h2</artifactId>
18 <version>2.2.224</version>
19 <scope>test</scope>
20 </dependency>
21 </dependencies>

Ejemplo: arranque en @BeforeAll

DB_CLOSE_DELAY=-1 evita que H2 cierre la base en memoria en cuanto se libera la última conexión (útil si varios tests comparten la misma JVM). En un escenario real, aquí ejecutarías el DDL o cargarías un schema.sql.

UsuarioDAOTest.java
JAVA
1 import org.junit.jupiter.api.*;
2 import static org.assertj.core.api.Assertions.*;
3
4 class UsuarioDAOTest {
5
6 private static UsuarioDAO usuarioDAO;
7 private static java.sql.Connection connection;
8
9 @BeforeAll
10 static void setup() throws Exception {
11 connection = java.sql.DriverManager.getConnection(
12 "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1", "sa", "");
13 // Ejecutar aquí script SQL: CREATE TABLE ...
14 usuarioDAO = new UsuarioDAO(connection);
15 }
16
17 @AfterAll
18 static void teardown() throws Exception {
19 if (connection != null && !connection.isClosed()) {
20 connection.close();
21 }
22 }
23
24 @Test
25 @DisplayName("Debería insertar y recuperar un usuario correctamente")
26 void debeInsertarUsuario() {
27 Usuario nuevo = new Usuario("Leo", "leo@example.com");
28 usuarioDAO.guardar(nuevo);
29 Usuario resultado = usuarioDAO.buscarPorEmail("leo@example.com");
30 assertThat(resultado).isNotNull();
31 assertThat(resultado.getNombre()).isEqualTo("Leo");
32 }
33 }

Cuando necesitas MySQL de verdad

Si el SQL es muy específico de MySQL, muchos equipos usan Testcontainers: un contenedor MySQL se levanta desde el test, obtienes la URL JDBC y ejecutas migraciones. Requiere Docker en la máquina o en CI. Otra opción es un servicio MySQL en el pipeline (GitHub Actions services, etc.) y variables de entorno para la URL de test.

Ejercicio práctico

Misión: Contrato explícito

Escribe en comentarios al inicio de tu clase de test tres cosas: qué motor usas en test (H2/MySQL), cómo se crea el esquema (script, Flyway, etc.) y si los tests pueden ejecutarse en paralelo o deben ser secuenciales.

Ko-fi
Donaciones
Apoyá cafeycodigo con un café en Ko-fi. Colaboradores: insignia, muro y zona exclusiva.