¿Qué es un test?
Un test es código que verifica que tu código funciona correctamente. En lugar de probar manualmente en el navegador, escribes tests que se ejecutan solos y te avisan si algo se rompe.
Tipos de tests
| Tipo | ¿Qué prueba? | ¿Usa BD? | Velocidad |
|---|---|---|---|
| Unit | Una clase o función aislada | No | Muy rápido |
| Feature | Un flujo completo (HTTP → BD) | Sí | Más lento |
Estructura de un test
class UserTest extends TestCase
{
public function test_nombre_descriptivo(): void
{
// 1. Arrange — preparar los datos
$user = User::factory()->create();
// 2. Act — ejecutar la acción
$resultado = $user->nombreCompleto();
// 3. Assert — verificar el resultado
$this->assertEquals('Juan Pérez', $resultado);
}
}
Los tests siguen el patrón AAA:
- Arrange — preparar
- Act — ejecutar
- Assert — verificar
Assertions más comunes
$this->assertEquals($esperado, $actual); // son iguales
$this->assertTrue($valor); // es verdadero
$this->assertFalse($valor); // es falso
$this->assertNull($valor); // es null
$this->assertCount(3, $coleccion); // tiene 3 elementos
$this->assertInstanceOf(User::class, $obj); // es instancia de User
// Para HTTP (Feature Tests)
$response->assertStatus(200); // código HTTP 200
$response->assertRedirect('/ruta'); // redirige
$response->assertViewHas('users'); // la vista tiene variable
$response->assertJson(['key' => 'value']); // respuesta JSON
// Para base de datos
$this->assertDatabaseHas('users', ['email' => 'juan@email.com']);
$this->assertDatabaseMissing('users', ['email' => 'juan@email.com']);
Unit Tests
Prueban una clase de forma aislada. Las dependencias se simulan con mocks.
class UserServiceTest extends TestCase
{
public function test_crea_usuario_correctamente(): void
{
// Mock — simula el repositorio sin tocar la BD
$repo = Mockery::mock(UserRepository::class);
$repo->shouldReceive('create')
->once()
->with(['name' => 'Juan'])
->andReturn(new User(['name' => 'Juan']));
$service = new UserService($repo);
$resultado = $service->crear(['name' => 'Juan']);
$this->assertEquals('Juan', $resultado->name);
}
}
Feature Tests
Prueban el flujo completo: request HTTP → controlador → base de datos → respuesta.
class UserControllerTest extends TestCase
{
use RefreshDatabase; // resetea la BD entre cada test
public function test_lista_usuarios(): void
{
User::factory()->count(3)->create();
$response = $this->get('/users');
$response->assertStatus(200);
$response->assertViewHas('users');
}
public function test_crea_usuario(): void
{
$response = $this->post('/users', [
'name' => 'Juan',
'email' => 'juan@email.com',
]);
$response->assertRedirect('/users');
$this->assertDatabaseHas('users', ['email' => 'juan@email.com']);
}
public function test_usuario_autenticado_puede_ver_perfil(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/perfil');
$response->assertStatus(200);
}
}
Factories
Generan datos de prueba de forma rápida y limpia.
// Crear un usuario en BD
$user = User::factory()->create();
// Crear sin guardar en BD
$user = User::factory()->make();
// Crear varios
$users = User::factory()->count(5)->create();
// Con datos específicos
$admin = User::factory()->create(['role' => 'admin']);
// Con relaciones
$user = User::factory()
->has(Order::factory()->count(3))
->create();
Object Mother
Patrón para centralizar la creación de objetos de prueba con nombres semánticos.
class UserMother
{
public static function admin(): User
{
return User::factory()->create(['role' => 'admin']);
}
public static function inactivo(): User
{
return User::factory()->create(['active' => false]);
}
public static function conPedidos(int $cantidad = 3): User
{
return User::factory()
->has(Order::factory()->count($cantidad))
->create();
}
}
// En tus tests:
$admin = UserMother::admin();
$inactivo = UserMother::inactivo();
Ventaja: los tests son más legibles y no repites configuración en cada archivo.
Mocks
Simulan dependencias para aislar la unidad que estás probando.
// Mock básico
$repo = Mockery::mock(UserRepository::class);
// Esperar que se llame un método
$repo->shouldReceive('find')->once()->andReturn($user);
// Esperar que NO se llame
$repo->shouldNotReceive('delete');
// Con parámetros específicos
$repo->shouldReceive('find')->with(1)->andReturn($user);
Ejecutar los tests
# Todos los tests
php artisan test
# Un archivo específico
php artisan test tests/Feature/UserControllerTest.php
# Un método específico
php artisan test --filter test_crea_usuario
# Con reporte de coverage en consola
php artisan test --coverage
# Con reporte de coverage en HTML
php artisan test --coverage-html reports/coverage
El reporte HTML se genera en la carpeta reports/coverage/ — ábrelo con el navegador para ver qué líneas están cubiertas y cuáles no.
Buenas prácticas
- Un test, una cosa — cada test verifica un solo comportamiento
- Nombres descriptivos —
test_usuario_inactivo_no_puede_iniciar_sesiones mejor quetest_login - Independencia — los tests no deben depender entre sí
- Usa
RefreshDatabase— para que cada test empiece con la BD limpia - Empieza por Feature Tests — dan más valor con menos esfuerzo
Estructura recomendada de carpetas
tests/
├── Unit/
│ ├── Services/
│ │ └── UserServiceTest.php
│ └── Models/
│ └── UserTest.php
├── Feature/
│ └── UserControllerTest.php
└── Mothers/
└── UserMother.php