09 March

Guía Básica de Tests con PHPUnit en Laravel

Greetik | 09/03/2026

¿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) 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 descriptivostest_usuario_inactivo_no_puede_iniciar_sesion es mejor que test_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

Etiquetas

Navegacion