php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

【Guía de campo completa】Estrategia de testing en Laravel — Pest/PHPUnit, Feature/Unit, Factories, BD, mocking de HTTP/Queue/Notification, Dusk y testing de regresión de UI de accesibilidad

Lo que aprenderás (puntos clave)

  • Guías de diseño para decidir “hasta dónde testear” en Laravel sin dudar (división de responsabilidades entre Unit/Feature/E2E)
  • Preparación de datos “difícil de romper” usando Factory/Seeder, y cómo escribir tests legibles
  • Patrones prácticos de testing para autenticación/autorización, APIs, validación, excepciones, colas, notificaciones y eventos
  • Cómo estabilizar dependencias inestables (APIs externas/HTTP, tiempo, aleatoriedad, storage) mediante Fake/Mock
  • Optimizaciones de CI (GitHub Actions, etc.) para ejecutar rápido (paralelización, división de tests, contramedidas ante tests inestables/flaky)
  • Cómo proteger la UI con Dusk y cómo abordar el testing de regresión de accesibilidad (foco, visualización de errores, ARIA) con ejemplos concretos

Lectores previstos (¿a quién le sirve?)

  • Ingenieros Laravel de nivel principiante a intermedio: quieres evitar “cosas que se rompen al añadir funciones” protegiéndolas con tests
  • Tech leads / responsables de operaciones: quieres calidad respaldada por CI que reduzca carga de revisión e incidentes
  • QA / diseñadores / especialistas de accesibilidad: quieres proteger continuamente una “experiencia operable” en formularios y listas

Nivel de accesibilidad: ★★★★★

Esta guía explica—con ejemplos concretos—cómo proteger regresiones de UI relacionadas con movimiento de foco, resúmenes de error, aria-invalid, aria-describedby y role="status"/alert usando Dusk y comprobaciones estáticas.


1. Introducción: los tests existen para crear un desarrollo “seguro de cambiar”

El desarrollo con Laravel se mueve rápido—y eso significa que los cambios ocurren con frecuencia. En proyectos con muchos cambios, lo más doloroso es: “lo arreglé, pero otra cosa se rompió”. Los tests no son un freno que ralentiza el trabajo de features; son un acelerador. Cuando puedes cambiar con confianza, las mejoras y los refactors se vuelven mucho más fáciles.

Pero si intentas testearlo todo, te quemas. La clave real es decidir el orden de escribir tests y las zonas críticas que debes proteger. Este artículo organiza patrones prácticos de testing en Laravel a lo largo de Unit/Feature/E2E (Dusk), y va más allá al cubrir también regresión de accesibilidad.


2. Decide primero: dividir responsabilidades entre Unit/Feature/E2E

Unit (tests unitarios)

  • Verificar rápidamente clases pequeñas (servicios, reglas de validación, lógica de agregación)
  • No tocar BD ni HTTP (si lo haces, muévelo a Feature)

Feature (comportamiento de la aplicación)

  • Cubre ruta → middleware → controller → BD → respuesta
  • Fija autenticación/autorización, validación, excepciones y respuestas JSON como “especificaciones”
  • En Laravel, suele ser lo más fácil de escribir y el mejor ROI

E2E (navegador, Dusk)

  • Protege regresiones de UI (formularios, ordenación, paginación, visualización accesible de errores, etc.)
  • Demasiados se vuelve lento, así que conviene concentrarse en recorridos clave del usuario

Proporción recomendada (guía aproximada)

  • Feature: 7
  • Unit: 2
  • Dusk: 1
    Este balance suele lograr tanto velocidad como confianza.

3. La base: Factories y “construcción legible de datos”

Si los tests son difíciles de leer, no se mantendrán—y se pudrirán. El truco es usar factories para generar datos de modo que la “intención” sea visible.

3.1 Ejemplo de Factory (User y Role)

// database/factories/UserFactory.php
public function definition(): array
{
    return [
        'name' => $this->faker->name(),
        'email' => $this->faker->unique()->safeEmail(),
        'password' => bcrypt('StrongPassw0rd!'),
        'tenant_id' => Tenant::factory(),
    ];
}

public function admin(): static
{
    return $this->afterCreating(function (User $user) {
        $user->assignRole('admin');
    });
}

3.2 Usa nombres que expliquen la situación

En los tests, los nombres de variables importan más que ser cortos.

  • En lugar de $user, usa $adminUser
  • En lugar de $p, usa $otherTenantProject

Esto por sí solo reduce bastante el costo de mantenimiento.


4. Bases de Feature tests: fijar HTTP y BD como “especificaciones”

4.1 Una página que requiere autenticación

public function test_dashboard_requires_login()
{
    $res = $this->get('/dashboard');
    $res->assertRedirect('/login');
}

4.2 Mostrar correctamente para usuarios logueados

public function test_dashboard_shows_for_logged_in_user()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    $res = $this->get('/dashboard');
    $res->assertOk()->assertSee('ダッシュボード');
}

4.3 Validación (422)

public function test_store_rejects_invalid_input()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    $res = $this->post('/projects', ['name' => '']); // violación de required
    $res->assertSessionHasErrors(['name']);
}

4.4 Éxito de creación (PRG + BD)

public function test_store_creates_project()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    $res = $this->post('/projects', ['name' => '新規プロジェクト']);
    $res->assertRedirect('/projects');

    $this->assertDatabaseHas('projects', [
        'name' => '新規プロジェクト',
        'tenant_id' => $user->tenant_id,
    ]);
}

Notas

  • Para páginas, assertSessionHasErrors se siente natural.
  • Para APIs, postJson() + fijar la estructura del JSON es muy potente (se cubre más adelante).

5. Tests de autorización (Policy/Gate): el lugar más importante para evitar fugas de frontera

La multi-tenancy y los permisos tienen un radio de explosión enorme si fallan, así que protegerlos con Feature tests tiene gran valor.

public function test_user_cannot_update_other_tenant_project()
{
    $t1 = Tenant::factory()->create();
    $t2 = Tenant::factory()->create();

    $user = User::factory()->create(['tenant_id' => $t1->id]);
    $other = Project::factory()->create(['tenant_id' => $t2->id]);

    $this->actingAs($user);
    app()->instance('tenant', $t1);

    $res = $this->patch("/projects/{$other->id}", ['name' => '侵入']);
    $res->assertForbidden();
}

Para mayor robustez, también verifica:

  • Se devuelve un 403
  • La base de datos no cambió

Eso hace aún más difícil que se cuelen regresiones.


6. Tests de API: convertir respuestas JSON en un “contrato”

6.1 Fijar una respuesta de éxito

public function test_api_returns_orders()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    Order::factory()->count(3)->create(['user_id' => $user->id]);

    $res = $this->getJson('/api/v1/orders');
    $res->assertOk()
        ->assertJsonStructure([
            'data' => [
                '*' => ['id','status','total']
            ],
            'meta' => ['page','per_page']
        ]);
}

6.2 Fijar un formato de error (problem+json)

Si tu API estandariza en application/problem+json, esto se vuelve uno de los guardarraíles más fuertes.

public function test_api_validation_returns_problem_json()
{
    $user = User::factory()->create();
    $this->actingAs($user);

    $res = $this->postJson('/api/v1/orders', ['items' => []]);
    $res->assertStatus(422)
        ->assertHeader('Content-Type', 'application/problem+json')
        ->assertJsonStructure(['type','title','status','detail','errors','trace_id']);
}

7. Estabiliza dependencias inestables con Fake/Mock (HTTP/Notifications/Mail/Events)

Una de las fortalezas de Laravel es la cantidad de fakes que ofrece para tests prácticos.

7.1 Fake de API externa (HTTP)

use Illuminate\Support\Facades\Http;

public function test_external_api_is_called()
{
    Http::fake([
        'api.example.com/*' => Http::response(['ok' => true], 200),
    ]);

    $res = $this->post('/sync');
    $res->assertRedirect();

    Http::assertSent(function ($req) {
        return str_contains($req->url(), 'api.example.com') && $req->method() === 'POST';
    });
}

7.2 Notificaciones

use Illuminate\Support\Facades\Notification;

public function test_notification_is_sent()
{
    Notification::fake();

    $user = User::factory()->create();
    $this->post('/password/reset', ['email' => $user->email]);

    Notification::assertSentTo($user, \App\Notifications\ResetPassword::class);
}

7.3 Mail

use Illuminate\Support\Facades\Mail;

public function test_invoice_mail_is_queued()
{
    Mail::fake();

    $user = User::factory()->create();
    $this->actingAs($user);

    $this->post('/invoices', ['plan' => 'pro']);

    Mail::assertQueued(\App\Mail\InvoiceCreated::class);
}

7.4 Eventos

use Illuminate\Support\Facades\Event;

public function test_event_dispatched_on_create()
{
    Event::fake();

    $user = User::factory()->create();
    $this->actingAs($user);

    $this->post('/projects', ['name'=>'A']);

    Event::assertDispatched(\App\Events\ProjectCreated::class);
}

8. Tiempo, aleatoriedad, colas: pequeños trucos para reproducibilidad

8.1 Fijar el tiempo (hace estables los tests)

use Illuminate\Support\Carbon;

public function test_due_date_is_calculated()
{
    Carbon::setTestNow('2026-01-14 10:00:00');

    $due = app(DueDateCalculator::class)->forPlan('pro');
    $this->assertEquals('2026-02-14', $due->toDateString());
}

8.2 Queue fake

use Illuminate\Support\Facades\Queue;

public function test_job_dispatched()
{
    Queue::fake();

    $this->post('/export');

    Queue::assertPushed(\App\Jobs\ExportCsv::class);
}

9. Ejecutar tests con BD rápido: RefreshDatabase y “haz pesado solo lo que deba ser pesado”

  • RefreshDatabase es legible, pero los resets de BD pueden ser costosos
  • Si la velocidad es crítica, dividir suites y usar BD solo donde haga falta puede ayudar
use Illuminate\Foundation\Testing\RefreshDatabase;

class ProjectTest extends TestCase
{
    use RefreshDatabase;
}

Además, crear demasiados datos con factory ralentiza, así que por defecto:

  • Conteos mínimos necesarios
  • Agregaciones con 1–3 registros
    Ejecuta tests de performance por separado para que el CI habitual se mantenga ligero.

10. Qué debe proteger Dusk: flujos críticos y accesibilidad

Dusk es potente pero se vuelve lento si se abusa. Un buen enfoque es proteger solo estos flujos:

  • Login
  • Formularios principales de creación (registro/compra/solicitud)
  • Filtrado/ordenación en listas importantes
  • Visualización crítica de errores (resumen de validación + comportamiento de foco)

10.1 Ejemplo: el foco se mueve al resumen de errores

public function test_error_summary_focuses_on_invalid_submit()
{
    $this->browse(function (\Laravel\Dusk\Browser $b) {
        $b->visit('/register')
          ->type('email', 'not-an-email')
          ->press('登録')
          ->assertPresent('#error-summary')
          ->assertFocused('#error-summary'); // verifica que el foco se movió
    });
}

10.2 Ejemplo: se aplica aria-invalid

public function test_aria_invalid_set_on_error()
{
    $this->browse(function (\Laravel\Dusk\Browser $b) {
        $b->visit('/register')
          ->press('登録')
          ->assertAttribute('#email', 'aria-invalid', 'true');
    });
}

No es realista cubrir “toda accesibilidad” con Dusk, pero estas áreas son propensas a regresión y vale la pena proteger:

  • Resúmenes de error
  • Errores de campos requeridos
  • Anuncios de finalización/progreso (role="status")

11. Consejos para “tests irrompibles”: contramedidas contra flaky y pequeñas decisiones de diseño

11.1 No dependas de aleatoriedad

  • No dependas demasiado de valores aleatorios de Faker
  • Usa strings fijos o state()
  • Mantén expectativas deterministas

11.2 No dependas del tiempo

  • Usa Carbon::setTestNow() para fijar comportamientos basados en tiempo

11.3 Usa esperas apropiadas en Dusk

  • Prefiere waitForText / waitFor en lugar de acumular pause()
  • Haz explícitas las “condiciones de finalización” (p. ej., añadir data-testid="loaded")

12. Ejecutar rápido en CI: un ejemplo mínimo y pensamiento operativo

12.1 Orden recomendado por velocidad

  1. Análisis estático (PHPStan/Pint)
  2. Unit/Feature
  3. Dusk (solo flujos clave)
  4. Tests de contrato si tienes margen (diffs de OpenAPI)

12.2 Cómo dividir tests

  • Para PRs normales: ejecutar solo Unit/Feature
  • En merges a main: ejecutar Dusk también
  • Nocturno: ejecutar tests pesados (muchos datos, performance)
    Esto mantiene alta la velocidad de desarrollo durante el día.

13. Deja “muestras” en los tests: valor como documentación

Los tests son especificaciones. Especialmente en estos casos, los tests suelen ser la documentación más clara:

  • Fronteras de autorización (quién puede hacer qué)
  • Formatos de error (contratos de API)
  • Redacción de validación (UX de inputs)
  • Idempotencia y protección contra doble envío (prevención de incidentes)

El estado ideal es: cuando los revisores preguntan “¿cuál es la spec?”, puedas señalar un test.


14. Errores comunes y cómo evitarlos

  • Depender demasiado del copy de la UI (muchos fallos por pequeños cambios de texto)
    • Evita: asertar solo texto clave; enfócate en estructura (estado, presencia, reglas)
  • Datos de test enormes ralentizan todo
    • Evita: conteos mínimos; datos pequeños para agregaciones; mover tests pesados a otra suite
  • Llamar a APIs externas de verdad
    • Evita: usa siempre HTTP fakes; fakea también patrones de fallo
  • No tener tests de autorización
    • Evita: cubre siempre límites de tenant y límites de rol con Feature tests
  • Ejecuciones Dusk inestables
    • Evita: condiciones de espera explícitas, eliminar dependencia de tiempo, mantener E2E mínimo

15. Checklist (para distribución)

Estrategia de tests

  • [ ] Decididos roles y proporción para Unit/Feature/Dusk
  • [ ] Protegidos flujos clave (registro/compra/solicitud/admin) con una suite Dusk mínima

Preparación de datos

  • [ ] Factories son legibles (state/admin/tenant, etc.)
  • [ ] Los nombres de variables transmiten intención ($adminUser, etc.)

Feature (“specs a proteger”)

  • [ ] Autenticación (redirect/401 si no está logueado)
  • [ ] Autorización (403, límite de tenant)
  • [ ] Validación (422, qué campos fallan)
  • [ ] Cambios en BD (assertDatabaseHas/Missing)

Fakes / estabilización

  • [ ] Fakes correctos para HTTP/Notification/Mail/Queue/Event
  • [ ] Tiempo fijado con Carbon::setTestNow()

Regresión de accesibilidad

  • [ ] Existencia del resumen de errores + foco (Dusk)
  • [ ] Básicos de aria-invalid / aria-describedby (Dusk)
  • [ ] role="status" de éxito/progreso no ha desaparecido (en el alcance necesario)

16. Resumen

En testing con Laravel, el camino más corto es solidificar los “contratos” de tu aplicación con Feature tests primero. Fija autenticación, autorización, validación y respuestas de API como especificaciones, y estabiliza dependencias externas con fakes. Usa Dusk de forma selectiva—protege solo flujos críticos—y se recomienda especialmente proteger regresiones de accesibilidad como la visualización de errores en formularios y el comportamiento del foco. Una vez que los tests están en su lugar, los cambios dan menos miedo. Los proyectos que pueden mejorar con seguridad tienden a crecer más rápido—y con más amabilidad.


Enlaces de referencia

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)