Guía Básica de Tests con PHPUnit en Laravel
Laravel2026-03-09 13:17:06Greetik Soluciones SLGuía Básica de Tests con PHPUnit en Laravel

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

¿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
UnitUna clase o función aisladaNoMuy rápido
FeatureUn flujo completo (HTTP → BD)Más lento

Estructura de un test

classUserTestextendsTestCase{
    publicfunctiontest_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.

classUserServiceTestextendsTestCase{
    publicfunctiontest_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.

classUserControllerTestextendsTestCase{
    useRefreshDatabase; // resetea la BD entre cada testpublicfunctiontest_lista_usuarios(): void{
        User::factory()->count(3)->create();

        $response = $this->get('/users');

        $response->assertStatus(200);
        $response->assertViewHas('users');
    }

    publicfunctiontest_crea_usuario(): void{
        $response = $this->post('/users', [
            'name'  => 'Juan',
            'email' => 'juan@email.com',
        ]);

        $response->assertRedirect('/users');
        $this->assertDatabaseHas('users', ['email' => 'juan@email.com']);
    }

    publicfunctiontest_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.

classUserMother{
    publicstaticfunctionadmin(): User{
        return User::factory()->create(['role' => 'admin']);
    }

    publicstaticfunctioninactivo(): User{
        return User::factory()->create(['active' => false]);
    }

    publicstaticfunctionconPedidos(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