[Guía completa lista para el campo] Automatización de tests en Laravel y verificación de accesibilidad — Pest/PHPUnit, Feature/E2E, Dusk, axe/Pa11y y configuración de CI
Qué aprenderás (destacados)
- Cómo elegir entre Pest y PHPUnit; diseñar tests para
Feature/Unit/Browser(Dusk) - Patrones prácticos para factories, seeding, ejecuciones en paralelo y mocks/fakes (Mail/Queue/Notification/Event)
- Cómo integrar checks de accesibilidad automatizados (axe-core/Pa11y) con Dusk/CI
- Validación E2E de navegación, foco, regiones en vivo y operaciones con teclado
- Cobertura, capturas ante fallos, estrategia de datos de prueba y consejos de rendimiento
- Una tubería CI/CD de ejemplo usando GitHub Actions
Lectores previstos (¿quién se beneficia?)
- Ingenieros Laravel de nivel principiante a intermedio: comprende la panorámica del testing y blinda tu proyecto frente a fallos
- Tech leads / QA: ejecuta E2E y checks de accesibilidad en cada pull request
- Diseñadores / redactores: incorpora garantías para lectores de pantalla en textos de UI y mensajes de error
Nivel de accesibilidad: ★★★★★
Usa Dusk junto con axe/Pa11y y automatiza la verificación hasta
role="status"/aria-live/aria-describedby/ traslados de foco / operaciones de teclado. Ante fallos, captura pantallas y logs y guárdalos en CI.
1. Introducción: los tests garantizan “robustez” y “responsabilidad”
Los objetivos del testing son congelar el comportamiento y proporcionar una red de seguridad para cambiar con confianza. Laravel trae testing HTTP, rollbacks de base de datos, diversos fakes y automatización de navegador real (Dusk). Superponer checks de accesibilidad te permite garantizar continuamente que sea visible, legible y operable. Dibuja primero el mapa y luego mejora paso a paso.
2. Diseño general: roles por capa de test
- Unit: Lógica pura de funciones/clases pequeñas. Evita I/O; corre en milisegundos.
- Feature (HTTP/DB): Rutas, validación, autorización, escrituras en DB, notificaciones—casos de uso.
- Browser (Dusk): Interacciones reales de navegador para verificar pantallas, foco, operaciones de teclado y anuncios.
- Checks de accesibilidad: Integra axe/Pa11y con Dusk/CI para atrapar defectos básicos.
- Contrato/snapshot: Detecta cambios rompientes en esquemas API (OpenAPI) o textos de UI.
En la práctica, empezar con Feature 70% / Unit 20% / E2E 10% equilibra bien coste e impacto.
3. Configuración: Pest, paralelismo, DB, factories
3.1 Instalar Pest (opcional)
composer require pestphp/pest --dev
php artisan pest:install
- Escribe tests estilo función bajo
tests/Featureytests/Unit. Puede coexistir con PHPUnit.
3.2 Base de datos de test y ejecuciones en paralelo
php artisan test --parallel
# o
php artisan test --parallel --processes=4
- Usa el trait
RefreshDatabasepara rollbacks transaccionales o migraciones. - Para paralelo, usa SQLite en memoria o aprovisiona MySQL de prueba por proceso.
3.3 Factories y transiciones de estado
// database/factories/PostFactory.php
$factory->define(Post::class, fn() => [
'title' => fake()->sentence(),
'body' => fake()->paragraph(),
'status'=> 'draft',
'published_at' => null,
]);
// Estado
public function published(): static {
return $this->state(fn() => ['status'=>'published','published_at'=>now()]);
}
- Usa estados para hacer explícitos borrador/publicado; los tests se leen mejor.
4. Tests Feature: bloquea casos de uso
4.1 Validación y autorización
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('can create an article', function () {
$user = User::factory()->create();
$this->actingAs($user);
$res = $this->post('/posts', ['title'=>'First Post','body'=>'Body']);
$res->assertRedirect('/posts');
$this->assertDatabaseHas('posts', ['title'=>'First Post','user_id'=>$user->id]);
});
test('guest cannot create', function () {
$this->post('/posts', ['title'=>'x','body'=>'y'])->assertRedirect('/login');
});
4.2 JSON API (Sanctum)
test('can fetch list via API', function () {
Sanctum::actingAs(User::factory()->create(), ['posts:read']);
Post::factory()->count(3)->published()->create();
$this->getJson('/api/v1/posts')
->assertOk()
->assertJsonCount(3, 'data')
->assertJsonStructure(['data'=>[['id','title','author'=>['id','name']]]]);
});
4.3 Verificar efectos colaterales con fakes
test('notification is sent on create', function () {
Notification::fake();
$user = User::factory()->create();
$this->actingAs($user)->post('/posts', ['title'=>'x','body'=>'y']);
Notification::assertSentTo($user, \App\Notifications\PostPublished::class);
});
4.4 URLs firmadas y rate limiting
test('expired URL is rejected', function () {
$url = URL::temporarySignedRoute('files.show', now()->subMinute(), ['id'=>1]);
$this->get($url)->assertForbidden();
});
test('comment posting is throttled', function () {
RateLimiter::for('comment', fn()=>[Limit::perMinute(5)->by('t')]);
$this->actingAs(User::factory()->create());
for($i=0;$i<5;$i++){ $this->post('/comments', ['body'=>'hi']); }
$this->post('/comments', ['body'=>'hi'])->assertStatus(429);
});
5. Tests de navegador (Dusk): protege la experiencia—operaciones y anuncios
5.1 Configuración
php artisan dusk:install
php artisan dusk
- Chromedriver incluido. Ejecuta headless en CI.
5.2 Flujo básico
// tests/Browser/RegisterTest.php
public function test_register_flow()
{
$this->browse(function (Browser $b) {
$b->visit('/register')
->type('#name','Hanako Yamada')
->type('#email','hanako@example.com')
->type('#password','StrongPassw0rd!')
->type('#password_confirmation','StrongPassw0rd!')
->check('agree')
->press('Sign up')
->waitForLocation('/dashboard')
->assertSee('Welcome');
});
}
5.3 Teclado, foco, regiones en vivo
public function test_error_summary_focus_and_readable()
{
$this->browse(function (Browser $b) {
$b->visit('/register')
->press('Sign up') // enviar vacío
->waitFor('#error-title')
->assertFocused('#error-title') // el foco va al resumen
->assertSeeIn('#error-title','Please review your input.')
->assertPresent('[role="alert"]')
->assertAttribute('#email','aria-invalid','true');
});
}
- En errores, asegúrate de que el foco se mueve al resumen de errores, que existe
role="alert"y que los campos tienenaria-invalid. - Anuncia progreso/resultados vía
role="status"/aria-live. En Dusk, verifica su presencia en el DOM.
5.4 Capturas y logs
public function test_capture_on_failure()
{
$this->browse(function (Browser $b) {
$b->visit('/')->screenshot('home'); // storage/screenshots/home.png
});
}
- La captura automática ante fallo es el camino de depuración más corto. Guárdala siempre como artefacto en CI.
6. Checks automatizados de accesibilidad: conecta axe/Pa11y con Dusk/CI
6.1 Dusk × axe-core (integración mínima)
npm i -D axe-core- Inyecta
axe.min.jsen la página desde Dusk y evalúa.
public function test_accessibility_smoke()
{
$this->browse(function (Browser $b) {
$b->visit('/register');
// Inyectar axe
$axe = file_get_contents(base_path('node_modules/axe-core/axe.min.js'));
$b->script($axe.'; void(0);');
// Ejecutar y recuperar resultados
$results = $b->script('return axe.run(document, { resultTypes: ["violations"] });')[0];
// Filtro simple para serious/critical
$violations = collect($results['violations'] ?? [])->filter(function($v){
return in_array('serious', $v['impact'] ?? []) || in_array('critical', $v['impact'] ?? []);
});
if ($violations->isNotEmpty()) {
logger()->error('axe.violations', ['violations'=>$violations]);
}
$this->assertTrue($violations->isEmpty(), 'Accessibility violations detected');
});
}
- Empieza con un smoke que detecta problemas severos.
- Ajusta reglas detalladas en CI con Pa11y para una operación más sencilla.
6.2 Pa11y (CLI) para checks programados en múltiples páginas
npm i -D pa11y pa11y-ci- Crea
pa11y-ci.json.
{
"defaults": {
"timeout": 30000,
"standard": "WCAG2AA",
"wait": 1000,
"chromeLaunchConfig": { "args": ["--no-sandbox","--disable-dev-shm-usage"] }
},
"urls": [
"http://localhost:8000/",
"http://localhost:8000/register",
"http://localhost:8000/login",
"http://localhost:8000/posts"
]
}
- En CI, arranca el servidor de Laravel y ejecuta
npx pa11y-ci.
- Enumera páginas en JSON. Para falsos positivos, configura excepciones de reglas selectivamente.
- Usa Dusk para cubrir experiencia (traslados de foco, actualizaciones en vivo) que Pa11y puede no detectar.
7. Verificando textos de UI, mensajes de error y anuncios
- Mantén los mensajes de error cortos y específicos; vincula entradas vía
aria-describedby. - Usa
role="status"/role="alert"según prioridad para toasts/banners. - No dependas “solo del color”; añade texto/iconos—comprobable vía DOM en Dusk.
- Para i18n, verifica
<html lang>y language-of-parts (según tu política).
8. Diseño de datos de prueba: realista, no sobre-fabricado
- Los valores de factory deben verse plausibles. Respeta formatos reales para dirección/teléfono/precio.
- Usa patrón builder para componer precondiciones complejas (p. ej., post publicado + 3 tags + 2 comentarios).
- Para rendimiento con datos grandes, usa Seeder; en E2E, prefiere IDs fijos para referenciarlos fácil.
9. Aceleraciones: elimina cuellos de botella
- Paralelismo + SQLite en memoria para arranques rápidos.
- Stub de APIs externas con
Http::fake()para evitar red. - Enfoca Dusk en lo esencial; comprime escenarios pesados a 1–3 casos.
- Estabiliza feeds/listas E2E con mocks de API y centra los checks a11y en la UI.
10. CI/CD: plantilla GitHub Actions (MySQL/Redis/Node en paralelo)
name: test
on: [push, pull_request]
jobs:
laravel:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8
env:
MYSQL_DATABASE: testing
MYSQL_USER: user
MYSQL_PASSWORD: secret
MYSQL_ROOT_PASSWORD: root
ports: ["3306:3306"]
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -u root -proot"
--health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:alpine
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, pdo_mysql, intl
coverage: none
- name: Cache composer
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
- run: composer install --no-interaction --prefer-dist
- name: Setup Node
uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npm run build --if-present
- name: App key & env
run: |
cp .env.example .env
php artisan key:generate
php artisan migrate --force
- name: Pest / PHPUnit
run: php artisan test --parallel --processes=4
- name: Dusk (headless)
run: php artisan dusk --env=dusk.local
env:
APP_URL: http://127.0.0.1:8000
DB_DATABASE: testing
DB_USERNAME: user
DB_PASSWORD: secret
- name: Start server for Pa11y
run: php -S 127.0.0.1:8000 -t public & echo $! > server.pid && sleep 3
- name: Pa11y CI
run: npx pa11y-ci
- name: Upload artifacts (screenshots)
uses: actions/upload-artifact@v4
if: failure()
with:
name: dusk-screens
path: storage/screenshots
- Sube siempre capturas de fallos.
- Habilita
intlpara que pasen tests de i18n/número/fecha.
11. Recetas frecuentes de fake/mock
Mail::fake();
Queue::fake();
Event::fake();
Notification::fake();
Http::fake([
'https://api.example.com/*' => Http::response(['ok'=>true], 200),
]);
Storage::fake('s3'); // verificar subidas
- Verifica de forma segura que “lo enviaste”. No olvides afirmar destinatario/asunto/contador.
- Para imágenes/PDFs, usa dummies y verifica solo que hubo procesamiento.
12. Perspectivas E2E que protegen la “legibilidad”
- El anillo de foco permanece visible (Tab lo hace evidente).
- Botones/enlaces son alcanzables solo con teclado.
- Usa
role="alert"para avisos críticos;role="status"para regulares;aria-livepor defectopolite. - Las imágenes tienen
alt; las decorativas usanalt="". - Elementos muy animados respetan
prefers-reduced-motion. - No dependas solo del color; usa texto/iconos/bordes como señales redundantes.
En Dusk, apóyate en aserciones del DOM y, cuando sea necesario, verifica la presencia de estilos (evita depender de valores de color exactos).
13. Muestras representativas (extractos)
13.1 a11y para errores de validación
public function test_validation_errors_have_describedby()
{
$this->browse(function (Browser $b) {
$b->visit('/register')
->press('Sign up')
->waitFor('#error-title')
->assertPresent('#email-error')
->assertAttribute('#email','aria-describedby', fn($v) => str_contains($v,'email-error'));
});
}
13.2 Anuncio en toast
public function test_toast_announces_status()
{
$this->browse(function (Browser $b) {
$b->visit('/profile')
->press('Save')
->waitFor('[role="status"]')
->assertSeeIn('[role="status"]','Saved');
});
}
13.3 Texto alternativo
test('images have alt text', function () {
$html = $this->get('/')->getContent();
expect($html)->toContain('alt=');
});
14. Escollos y soluciones
- Intentarlo todo solo con Dusk → lento y frágil. Haz Feature el núcleo y cubre experiencias clave con Dusk.
- Confiar solo en checks a11y automáticos → traslados de foco y actualizaciones en vivo son difíciles de detectar. Complementa con E2E.
- Datos de prueba aleatorios cada vez → difícil reproducir fallos. Estabiliza con IDs fijos o seeds.
- Olvidar restaurar fakes → Prefiere setup/teardown compartidos (hooks de Pest o
setUp/tearDownde PHPUnit) frente a “un test, un assert”. - Desajustes Chrome/DB en CI → Incluye siempre health checks; usa
--disable-dev-shm-usagepara evitar problemas de memoria. - Falta de capturas ante fallos → Coste de debug se dispara. El almacenamiento de artefactos debe ser estándar.
15. Lista de verificación (lista para entregar)
Estrategia
- [ ] Haz Feature (HTTP/DB) el núcleo; Unit para lógica; E2E solo lo esencial
- [ ] Ejecuta Pest/Feature + Dusk + a11y (Pa11y/axe) en cada PR
Datos
- [ ] Diseño de estados en factories (
draft/published, etc.) - [ ] Escenarios reproducibles con seeds e IDs fijos
Accesibilidad
- [ ] El foco va al resumen de errores;
role="alert"/aria-invalid/aria-describedby - [ ] Anuncia progreso/finalización con
role="status"/aria-live - [ ]
alten imágenes, decorativas conalt="", diseños no dependientes del color - [ ] Totalmente operable por teclado
Checks automatizados
- [ ] Smoke de Dusk + axe
- [ ] CI ejecuta lista de URLs en Pa11y
- [ ] Define umbrales de fallo por severidad
Velocidad/estabilidad
- [ ] Paralelismo; SQLite en memoria (o múltiples DBs)
- [ ] Fakes para Http/Mail/Queue/Storage
- [ ] Guarda capturas/logs de fallos como artefactos
CI
- [ ] Chrome headless
- [ ] Health checks para servicios (DB/Redis)
- [ ] Cache de dependencias (composer/npm)
16. Resumen
- Asigna roles claros a Pest/PHPUnit, Feature y Dusk para hacer seguros los cambios diarios.
- Introduce axe/Pa11y para prevenir automáticamente defectos a11y comunes.
- Verifica la esencia de la experiencia—traslados de foco, regiones en vivo, teclado—vía E2E.
- Mantén capturas y logs en CI para que los fallos te enseñen.
- Comienza con un smoke; amplía páginas y reglas gradualmente según resultados.
Espero que tu proyecto crezca robusto y usable por todas las personas. Toma esta plantilla como estándar por defecto del equipo si te encaja.
Calificación de accesibilidad de este contenido: ★★★★★ (muy alta)
- Garantías continuas para anuncios/foco/independencia del color mediante checks a11y automatizados y E2E.
- Incluye responsabilidad operativa (capturas/logs/IDs de petición) ante fallos.
- Mejoras futuras: testing exploratorio manual regular con lectores de pantalla principales (NVDA/JAWS/VoiceOver/TalkBack).
Enlaces de referencia
- Laravel (oficial)
- Pest / PHPUnit
- Automatización de navegador / accesibilidad
- CI / navegador headless
