【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-describedbyyrole="status"/alertusando 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,
assertSessionHasErrorsse 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”
RefreshDatabasees 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/waitForen lugar de acumularpause() - 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
- Análisis estático (PHPStan/Pint)
- Unit/Feature
- Dusk (solo flujos clave)
- 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.
