22 Sep 2024
Las pruebas de integración son fundamentales para garantizar que los diferentes componentes de nuestra aplicación funcionen correctamente juntos. En este post, dejo los pasos para configurar pruebas de integración en PHP utilizando PHPUnit, Docker y Phinx para las migraciones de base de datos.
Configurar un entorno de pruebas de integración puede ser complejo, pero con las herramientas adecuadas, podemos crear un sistema robusto y eficiente. Si usas Symfony, tal vez te interese mirar el tutorial PHPUnit: Integration Testing with Live Services, ya que existen algunos métodos ya contemplados. En este post me centraré en una implementación independiente de frameworks. Vamos a analizar los componentes clave de nuestra configuración:
El código fuente de este post está disponible en GitHub.
Para mantener nuestro proyecto organizado, voy a usar la siguiente estructura de directorios:
mkdir -p src tests/Unit tests/Integration db/migrations
Los tests unitarios van en tests/Unit
, mientras que las pruebas de integración se colocarán en tests/Integration
. Esto permite ejecutar los tests de forma independiente. Luego, el directorio migrations
es para Phinx.
Para ejecutar solo las pruebas de integración, se utiliza el siguiente comando:
./vendor/bin/phpunit --testsuite Integration
En cambio, si ejecutamos el comando sin argumentos, phpunit
pasará por todas las pruebas.
Utilizamos Docker para crear un entorno de base de datos aislado y reproducible:
version: '3.8'
services:
db:
image: mysql:8.0
environment:
- MYSQL_DATABASE=testdb
- MYSQL_ROOT_PASSWORD=rootpassword
tmpfs:
- /var/lib/mysql
ports:
- "3306:3306"
Este archivo configura un contenedor MySQL con una base de datos temporal para nuestras pruebas. El uso de tmpfs
asegura que los datos se almacenen en memoria, lo que acelera las pruebas y garantiza un estado limpio en cada ejecución (aquí hay más tips sobre optimizaciones para tests).
Vamos a instalar Phinx y PHPUnit en nuestro proyecto:
composer require robmorgan/phinx
composer require --dev phpunit/phpunit
Phinx nos ayuda a gestionar las migraciones de la base de datos. Vamos a crear el archivo phinx.php
en la raíz de nuestro proyecto:
<?php
return [
'paths' => [
'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations',
'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds',
],
'environments' => [
// ... (other configs)
'test' => [
'adapter' => 'mysql',
'host' => getenv('DB_HOST'),
'name' => getenv('DB_NAME'),
'user' => getenv('DB_USER'),
'pass' => getenv('DB_PASS'),
'port' => getenv('DB_PORT'),
'charset' => getenv('DB_CHARSET'),
],
],
];
Esta configuración define un entorno de prueba que coincide con nuestro contenedor Docker, permitiéndonos ejecutar migraciones en nuestra base de datos de prueba.
Para PHPUnit, vamos a crear un archivo de configuración phpunit.xml
en la raíz de nuestro proyecto:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<php>
<env name="DB_HOST" value="127.0.0.1"/>
<env name="DB_PORT" value="3306"/>
<env name="DB_NAME" value="testdb"/>
<env name="DB_USER" value="root"/>
<env name="DB_PASS" value="rootpassword"/>
<env name="DB_CHARSET" value="utf8mb4"/>
</php>
</phpunit>
Esta clase base prepara el entorno para nuestras pruebas de integración:
<?php declare(strict_types=1);
namespace ExampleApp\Testing\Integration;
use PHPUnit\Framework\TestCase;
class IntegrationTestCase extends TestCase
{
protected static $bootstrapped = false;
public static function setUpBeforeClass(): void
{
if (!self::$bootstrapped) {
require_once __DIR__ . '/bootstrap.php';
self::$bootstrapped = true;
}
}
}
Esta clase se asegura de que nuestro entorno de pruebas se configure correctamente antes de ejecutar cualquier prueba de integración.
El archivo bootstrap inicializa nuestro entorno de pruebas:
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
$phinx = require __DIR__ . '/../../phinx.php';
$app = new Phinx\Console\PhinxApplication();
$wrap = new Phinx\Wrapper\TextWrapper($app);
$wrap->getMigrate('test');
Este script carga las dependencias necesarias y ejecuta las migraciones de la base de datos, asegurando que nuestra base de datos de prueba esté en el estado correcto antes de ejecutar las pruebas.
Vamos a comprobar que la configuración es correcta, creando un test de integración simple:
<?php declare(strict_types=1);
namespace ExampleApp\Testing\Integration;
use ExampleApp\Testing\Integration\IntegrationTestCase;
class CustomerRepositoryTest extends IntegrationTestCase
{
public function testInsertAndRetrieveCustomer(): void
{
$this->assertTrue(true);
}
}
Por ahora nos basta con una simple assertion
. Solo queremos asegurarnos de que la configuración de nuestro entorno de pruebas es correcta al extender IntegrationTestCase
.
# Inicializar el servicio de base de datos
docker compose up -d
# Ejecutar las pruebas de integración
./vendor/bin/phpunit --testsuite Integration
# Verificar la base de datos
docker compose exec db mysql -u root -p testdb -e "show tables"
Tras ingresar la contraseña rootpassword
, deberíamos ver las tablas creadas por Phinx. Ejemplo:
+------------------+
| Tables_in_testdb |
+------------------+
| phinxlog |
+------------------+
Para un ejemplo más práctico, vamos a crear una tabla de clientes y una clase de repositorio para interactuar con ella. Primero, creamos una migración en db/migrations
:
./vendor/bin/phinx create CreateCustomersTable
Este comando habrá creado el archivo db/migrations/YYYYMMDDHHMMSS_create_customers_table.php
. Vamos a modificarlo para crear la tabla de clientes:
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateCustomersTable extends AbstractMigration
{
public function up(): void
{
$table = $this->table('customers');
$table->addColumn('name', 'string')
->addColumn('email', 'string')
->create();
}
public function down(): void
{
throw new \RuntimeException('No way to go back!');
}
}
Si ahora ejecutamos ./vendor/bin/phpunix --testsuite Integration
, se ejecutarán las migraciones y deberíamos ver la tabla customers
en nuestra base de datos de prueba.
Ahora vamos a modificar el test de integración que creamos en la sección anterior, para validar insertar y recuperar un cliente:
<?php declare(strict_types=1);
namespace ExampleApp\Testing\Integration;
use ExampleApp\Customer;
use ExampleApp\CustomerRepository;
use ExampleApp\Testing\Integration\IntegrationTestCase;
class CustomerRepositoryTest extends IntegrationTestCase
{
private CustomerRepository $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = new CustomerRepository(
getenv('DB_HOST'),
getenv('DB_NAME'),
getenv('DB_USER'),
getenv('DB_PASS'),
getenv('DB_PORT'),
getenv('DB_CHARSET')
);
}
public function testInsertAndRetrieveCustomer(): void
{
$customer = new Customer();
$customer->setName('Juan Pérez');
$customer->setEmail('juan@example.com');
$insertedId = $this->repository->insert($customer);
$retrievedCustomer = $this->repository->findById($insertedId);
$this->assertNotNull($retrievedCustomer);
$this->assertEquals('Juan Pérez', $retrievedCustomer->name);
$this->assertEquals('juan@example.com', $retrievedCustomer->email);
}
}
Este test inicializa el repositorio usando las variables de entorno, crea un cliente, lo inserta en la base de datos y luego lo recupera. Verifica que los datos recuperados coincidan con los datos insertados.
Por último, añadimos las clases que satisfacen el test:
Este es el objeto Customer
, dentro de la carpeta src
:
<?php declare(strict_types=1);
namespace ExampleApp;
class Customer
{
public readonly string $name;
public readonly string $email;
public function __construct(public readonly ?int $id = null)
{
}
public function setName(string $name): void
{
$this->name = $name;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
}
Y este será el repositorio CustomerRepository
, también en la carpeta src
(aunque en un proyecto real, esto seguramente estaría en un namespace diferente):
<?php declare(strict_types=1);
namespace ExampleApp;
use PDO;
class CustomerRepository
{
private PDO $pdo;
public function __construct(
string $host,
string $db,
string $user,
string $pass,
string $port,
string $charset
) {
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset";
$this->pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
public function insert(Customer $customer): int
{
$sql = 'INSERT INTO customers (name, email) VALUES (:name, :email)';
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':name' => $customer->name,
':email' => $customer->email,
]);
return (int) $this->pdo->lastInsertId();
}
public function findById(int $id): ?Customer
{
$sql = 'SELECT id, name, email FROM customers WHERE id = :id';
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
$data = $stmt->fetch();
if (!$data) {
return null;
}
$customer = new Customer($id);
$customer->setName($data['name']);
$customer->setEmail($data['email']);
return $customer;
}
}
Solo queda ejecutar las pruebas:
./vendor/bin/phpunit --testsuite Integration
Si todo está configurado correctamente, deberíamos ver una salida similar a la siguiente:
...
OK (1 test, 3 assertions)
Con esta configuración, tenemos un entorno de pruebas de integración robusto y eficiente que no depende de ningún framework. Podemos ejecutar nuestras pruebas contra una base de datos aislada y garantizar que nuestras migraciones se apliquen correctamente antes de cada suite de pruebas.
En un proyecto real, posiblemente la estructura de directorios sea diferente, así como la inicialización del repositorio. Sin embargo, estos pasos básicos deberían ser suficientes para comenzar a escribir pruebas de integración en PHP con Docker y Phinx.
¡Gracias por leer!
¡Tu mensaje fue recibido! Una vez que sea aprobado, estará visible para los demás visitantes.
Cerrar