Introducción: Por qué las pruebas en PHP no son una opción, sino un estándar
En el desarrollo moderno de PHP, las pruebas hace tiempo que dejaron de ser "buena práctica" y se convirtieron en una etapa obligatoria en proyectos de producción. Sin cobertura de código con pruebas, corres el riesgo de lanzar a producción un error que puede costarle dinero y reputación al negocio. En este artículo analizaremos tres niveles de pruebas: unit (pruebas unitarias), integration (pruebas de integración) y e2e (pruebas de extremo a extremo). Aprenderás en qué se diferencian, qué herramientas usar (PHPUnit, Pest, Laravel Dusk, Codeception) y cómo construir una estrategia de pruebas adecuada para tu proyecto PHP.
1. Pruebas unitarias: probando unidades de código aisladas
Las pruebas unitarias verifican el funcionamiento de clases, funciones o métodos individuales de forma aislada de dependencias externas (BD, API, sistema de archivos). Son el tipo de prueba más rápido y económico. La herramienta principal en PHP es PHPUnit (el estándar de facto) y su alternativa "moderna" Pest.
1.1. Instalación y primera prueba con PHPUnit
Instala PHPUnit a través de Composer:
composer require --dev phpunit/phpunitCrea una prueba para una clase calculadora simple:
<?php// src/Calculator.phpclass Calculator { public function add(int $a, int $b): int { return $a + $b; }}
// tests/CalculatorTest.phpuse PHPUnit\\Framework\\TestCase;
class CalculatorTest extends TestCase { public function testAddition(): void { $calc = new Calculator(); $result = $calc->add(2, 3); $this->assertEquals(5, $result); }}Ejecuta la prueba:
./vendor/bin/phpunit tests1.2. Uso de objetos Mock
Para aislar la clase bajo prueba, "reemplazamos" sus dependencias usando mocks. Ejemplo con un repositorio de usuarios:
<?phpclass UserService { private UserRepository $repo; public function __construct(UserRepository $repo) { $this->repo = $repo; } public function getUserName(int $id): string { $user = $this->repo->findById($id); return $user ? $user->getName() : 'Guest'; }}
// En la prueba$mockRepo = $this->createMock(UserRepository::class);$mockRepo->method('findById') ->with(1) ->willReturn(new User('Alice'));
$service = new UserService($mockRepo);$this->assertEquals('Alice', $service->getUserName(1));Best practice: Escribe pruebas unitarias para toda la lógica de negocio. Deben ejecutarse en segundos y no depender del entorno.
2. Pruebas de integración: verificando la interacción con BD, API y servicios externos
Las pruebas de integración verifican cómo tus clases funcionan juntas con componentes reales: base de datos, caché, servicios HTTP externos. Son más lentas que las pruebas unitarias, pero brindan mayor confianza en que el sistema funciona como un todo unificado.
2.1. Pruebas de trabajo con base de datos (Laravel + RefreshDatabase)
Si usas Laravel, las pruebas de integración se vuelven muy sencillas gracias al trait RefreshDatabase:
<?phpnamespace Tests\\Feature;
use Tests\\TestCase;use Illuminate\\Foundation\\Testing\\RefreshDatabase;
class UserRegistrationTest extends TestCase { use RefreshDatabase;
public function test_user_can_register(): void { $response = $this->post('/register', [ 'name' => 'John', 'email' => 'john@example.com', 'password' => 'secret123', ]);
$response->assertStatus(302); // redirect after success $this->assertDatabaseHas('users', [ 'email' => 'john@example.com' ]); }}El trait RefreshDatabase migra automáticamente la BD antes de las pruebas y