Los tests de integración verifican que tu código funciona correctamente con sus dependencias reales: la base de datos, el sistema de ficheros, servicios externos A diferencia de los tests unitarios (que usan mocks), los tests de integración ejecutan queries reales contra una BD de test. Son más lentos pero detectan errores que los mocks no pueden capturar: queries incorrectas, restricciones de integridad, índices que faltan.
Preparar una BD de test en PHPUnit
La clave es usar una BD separada (o SQLite en memoria para tests muy rápidos) y resetear su estado entre tests para garantizar el aislamiento:
<?php
// phpunit.xml
// <env name="DB_DSN" value="mysql:host=127.0.0.1;dbname=mi_app_test"/>
// <env name="DB_USER" value="test"/>
// <env name="DB_PASS" value="test"/>
DatabaseTestCase: transacciones con rollback en cada test
La técnica más eficiente: envolver cada test en una transacción y hacer rollback al final. La BD nunca queda sucia y no necesitas truncar tablas entre tests:
<?php
namespace TestsIntegration;
use PHPUnitFrameworkTestCase;
abstract class DatabaseTestCase extends TestCase {
protected PDO $pdo;
protected function setUp(): void {
$this->pdo = new PDO(
$_ENV['DB_DSN'],
$_ENV['DB_USER'],
$_ENV['DB_PASS'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$this->pdo->beginTransaction(); // Envolver el test en una transacción
}
protected function tearDown(): void {
$this->pdo->rollBack(); // Deshacer todos los cambios del test
}
}
Fixtures en setUp()
<?php
class UsuarioRepositorioTest extends DatabaseTestCase {
private UsuarioRepositorio $repositorio;
protected function setUp(): void {
parent::setUp(); // Inicia la transacción
$this->repositorio = new UsuarioRepositorio($this->pdo);
// Fixtures: datos de prueba para este test
$this->pdo->exec(
"INSERT INTO usuarios (id, nombre, email, activo) VALUES
(1, 'Ana García', '[email protected]', 1),
(2, 'Luis Pérez', '[email protected]', 0)"
);
}
public function testBuscarPorEmailDevuelveUsuarioCorrecto(): void {
$usuario = $this->repositorio->buscarPorEmail('[email protected]');
$this->assertNotNull($usuario);
$this->assertSame('Ana García', $usuario->nombre);
$this->assertSame(1, $usuario->activo);
}
public function testBuscarPorEmailDevuelveNullSiNoExiste(): void {
$usuario = $this->repositorio->buscarPorEmail('[email protected]');
$this->assertNull($usuario);
}
}
Ejemplo real: test de repositorio CRUD
<?php
class ProductoRepositorioIntTest extends DatabaseTestCase {
private ProductoRepositorio $repo;
protected function setUp(): void {
parent::setUp();
$this->repo = new ProductoRepositorio($this->pdo);
}
public function testCrearYRecuperarProducto(): void {
$id = $this->repo->crear([
'nombre' => 'Teclado mecánico',
'precio' => 79.99,
'categoria' => 'periféricos',
]);
$this->assertIsInt($id);
$this->assertGreaterThan(0, $id);
$producto = $this->repo->buscarPorId($id);
$this->assertSame('Teclado mecánico', $producto['nombre']);
$this->assertSame(79.99, (float) $producto['precio']);
}
public function testActualizarPrecio(): void {
$id = $this->repo->crear(['nombre' => 'Ratón', 'precio' => 25.0]);
$this->repo->actualizarPrecio($id, 19.99);
$producto = $this->repo->buscarPorId($id);
$this->assertSame(19.99, (float) $producto['precio']);
}
public function testEliminarBorraElRegistro(): void {
$id = $this->repo->crear(['nombre' => 'Monitor', 'precio' => 299.0]);
$this->repo->eliminar($id);
$this->assertNull($this->repo->buscarPorId($id));
}
public function testListarDevuelveOrdenadosPorPrecio(): void {
$this->repo->crear(['nombre' => 'C', 'precio' => 300.0]);
$this->repo->crear(['nombre' => 'A', 'precio' => 100.0]);
$this->repo->crear(['nombre' => 'B', 'precio' => 200.0]);
$lista = $this->repo->listarPorPrecio();
$this->assertSame('A', $lista[0]['nombre']);
$this->assertSame('B', $lista[1]['nombre']);
$this->assertSame('C', $lista[2]['nombre']);
}
}
Mock vs integración real: cuándo usar cada uno
| Aspecto | Mock | Test de integración real |
|---|---|---|
| Velocidad | Muy rápido (ms) | Más lento (decenas de ms) |
| Detecta errores de SQL | No | Sí |
| Detecta errores de índice | No | Sí |
| Detecta problemas N+1 | No | Sí |
| Requiere BD configurada | No | Sí |
La regla práctica: usa mocks para la lógica de negocio y tests de integración para los repositorios y servicios que interactúan con la BD.
Configurar la BD de test en CI/CD
# .github/workflows/tests.yml
jobs:
test:
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: mi_app_test
MYSQL_USER: test
MYSQL_PASSWORD: test
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s
steps:
- run: php artisan migrate --env=testing
- run: vendor/bin/phpunit --testsuite=integration
