Tests de navegador
Com fer tests de navegador a Laravel amb Dusk: automatització del navegador, formularis, JavaScript, esperes, pàgines i screenshots.
Per què tests de navegador?#
Els tests HTTP de Laravel ($this->get(), $this->post()) simulen peticions al servidor i verifiquen les respostes, però no executen JavaScript ni interactuen amb el DOM com ho faria un usuari real. Això és perfecte per a la majoria de tests, però hi ha funcionalitats que depenen del navegador: formularis dinàmics que mostren i oculten camps, interaccions amb drag-and-drop, modals que apareixen i desapareixen, selectors de dates, autocompletat, validació en temps real i qualsevol comportament que depengui de JavaScript.
Laravel Dusk és un paquet de testing de navegador que utilitza un ChromeDriver real per controlar un navegador Chrome/Chromium. Quan un test de Dusk fa clic a un botó, el clic passa realment en un navegador. Quan escriu text en un camp, el text apareix al camp. Quan verifica que un element és visible, comprova el CSS computed de l'element. Això dóna un nivell de confiança que els tests HTTP no poden proporcionar.
El desavantatge és la velocitat: els tests de navegador són ordres de magnitud més lents que els tests HTTP. Un test HTTP típic s'executa en 50-200ms, mentre que un test de Dusk pot trigar 2-10 segons. Per això, l'estratègia recomanada és tenir una base àmplia de tests HTTP per a la lògica del servidor i un conjunt més reduït de tests de Dusk per a les interaccions que depenen del navegador.
Instal·lació#
Dusk s'instal·la com a dependència de desenvolupament. La comanda dusk:install crea els fitxers de configuració, el directori de tests i descarrega el ChromeDriver compatible amb la versió de Chrome instal·lada al sistema:
composer require --dev laravel/dusk
php artisan dusk:installAixò crea:
tests/Browser/- Directori per als tests de navegadortests/Browser/Pages/- Directori per als Page Objectstests/Browser/Components/- Directori per als Component Objectstests/Browser/screenshots/- Captures de pantalla automàtiques en cas d'errortests/Browser/console/- Logs de la consola del navegadortests/DuskTestCase.php- Classe base per als tests de Dusk
Dusk utilitza un ChromeDriver real i necessita Chrome o Chromium instal·lat al sistema. En entorns CI/CD (GitHub Actions, GitLab CI), cal instal·lar Chrome al pipeline.
Crear i executar tests#
# Crear un test de navegador
php artisan dusk:make LoginTest
# Executar tots els tests de navegador
php artisan dusk
# Executar un test específic
php artisan dusk --filter=LoginTest
# Executar amb el navegador visible (no headless)
php artisan dusk --browseLa comanda php artisan dusk arrenca un servidor de desenvolupament automàticament (si no n'hi ha un executant-se), obre un navegador Chrome en mode headless (sense interfície gràfica) i executa els tests. L'opció --browse mostra el navegador perquè puguis veure les interaccions en temps real, cosa que és molt útil per depurar tests.
Escriure un test bàsic#
Els tests de Dusk utilitzen el mètode browse() que rep un callback amb una instància de Browser. Aquesta instància proporciona mètodes per navegar, interactuar amb elements i verificar l'estat de la pàgina:
namespace Tests\Browser;
use App\Models\User;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class LoginTest extends DuskTestCase
{
public function test_user_can_login(): void
{
$user = User::factory()->create([
'email' => 'joan@example.com',
]);
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('email', $user->email)
->type('password', 'password')
->press('Iniciar sessió')
->assertPathIs('/dashboard')
->assertSee("Benvingut, {$user->name}");
});
}
public function test_login_fails_with_wrong_password(): void
{
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('email', $user->email)
->type('password', 'contrasenya-incorrecta')
->press('Iniciar sessió')
->assertPathIs('/login')
->assertSee('Les credencials no són correctes');
});
}
}Cal tenir en compte que els tests de Dusk no utilitzen RefreshDatabase. La base de dades dels tests de Dusk és persistent entre tests perquè el servidor de desenvolupament i els tests s'executen en processos separats (les transaccions d'un procés no són visibles per l'altre). Per netejar la base de dades entre tests, utilitza DatabaseMigrations que fa migrate:fresh abans de cada test.
Interaccions amb el navegador#
Navegar i fer clic#
$browser->visit('/articles')
->clickLink('Llegir més')
->assertPathIs('/articles/1');
// Fer clic en un element per selector CSS
$browser->click('.btn-primary');
$browser->click('#submit-form');
$browser->click('[data-action="delete"]');
// Fer clic en un botó pel seu text
$browser->press('Enviar');
$browser->press('Confirmar eliminació');
// Navegar endavant i enrere
$browser->back();
$browser->forward();
$browser->refresh();Formularis#
Dusk proporciona mètodes intuïtius per interactuar amb tots els tipus d'elements de formulari:
// Camps de text
$browser->type('name', 'Joan Pere');
$browser->type('email', 'joan@example.com');
// Netejar i escriure
$browser->clear('name');
$browser->type('name', 'Nou nom');
// Afegir text sense netejar
$browser->append('description', ' text addicional');
// Select
$browser->select('country', 'AD'); // Seleccionar per valor
$browser->select('country'); // Seleccionar opció aleatòria
// Checkboxes
$browser->check('terms');
$browser->uncheck('newsletter');
// Radio buttons
$browser->radio('plan', 'premium');
// File inputs
$browser->attach('avatar', __DIR__ . '/fixtures/avatar.jpg');
// Camps amb date picker o formats especials
$browser->keys('#date-field', '15', '06', '2024');Exemple complet de formulari#
public function test_user_can_create_article(): void
{
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit('/articles/create')
->type('title', 'El meu article sobre Laravel')
->type('body', 'Contingut de l\'article amb informació detallada sobre Laravel i el seu ecosistema.')
->select('category_id', '3')
->check('is_featured')
->attach('cover_image', __DIR__ . '/fixtures/cover.jpg')
->press('Publicar')
->assertPathIs('/articles')
->assertSee('El meu article sobre Laravel')
->assertSee('Article publicat correctament');
});
}El mètode loginAs() és un shortcut que autentica l'usuari directament sense haver de passar pel formulari de login. Això accelera els tests que necessiten autenticació però no estan testejant el login en si.
Esperes (Waiting)#
Una de les complexitats dels tests de navegador és que les accions no són instantànies. Quan fas clic a un botó que fa una petició AJAX, la resposta triga uns mil·lisegons. Quan un modal apareix amb una animació, triga uns centenars de mil·lisegons. Si el test verifica el resultat immediatament, pot fallar perquè l'acció encara no ha acabat.
Dusk proporciona mètodes d'espera que pauses el test fins que una condició es compleixi o fins que passi un temps màxim:
// Esperar que un text aparegui (fins a 5 segons per defecte)
$browser->waitForText('Operació completada');
// Esperar que un element sigui visible
$browser->waitFor('.modal');
$browser->waitFor('#results-table');
// Esperar que un element desaparegui
$browser->waitUntilMissing('.loading-spinner');
$browser->waitUntilMissing('.modal');
// Esperar amb timeout personalitzat (en segons)
$browser->waitForText('Processant...', 10);
$browser->waitFor('.results', 15);
// Esperar que una localització canviï
$browser->waitForLocation('/dashboard');
$browser->waitForRoute('articles.show', ['id' => 1]);
// Esperar una petició AJAX (espera que no hi hagi peticions actives)
$browser->waitUntilVueIsNotRefreshing();
// Esperar que JavaScript retorni true
$browser->waitUntil('window.dataLoaded === true');
// Esperar i fer una acció quan la condició es compleix
$browser->whenAvailable('.modal', function ($modal) {
$modal->assertSee('Estàs segur?')
->press('Confirmar');
});Exemple amb interacció AJAX#
public function test_live_search_shows_results(): void
{
Article::factory()->create(['title' => 'Laravel Eloquent']);
Article::factory()->create(['title' => 'Laravel Blade']);
Article::factory()->create(['title' => 'React Components']);
$this->browse(function (Browser $browser) {
$browser->visit('/articles')
->type('#search', 'Laravel')
->waitFor('.search-results')
->assertSee('Laravel Eloquent')
->assertSee('Laravel Blade')
->assertDontSee('React Components');
});
}Assertions del navegador#
Dusk proporciona assertions específiques per verificar l'estat del navegador i de la pàgina:
// Text i contingut
$browser->assertSee('Text visible');
$browser->assertDontSee('Text absent');
$browser->assertSeeIn('.alert', 'Missatge d\'èxit');
$browser->assertSeeLink('Veure articles');
// URL i ruta
$browser->assertPathIs('/articles');
$browser->assertPathIsNot('/login');
$browser->assertPathBeginsWith('/articles');
$browser->assertRouteIs('articles.index');
$browser->assertQueryStringHas('page', '2');
$browser->assertFragmentIs('comments');
// Títol
$browser->assertTitle('Articles - Laravel Andorra');
$browser->assertTitleContains('Articles');
// Elements
$browser->assertPresent('#sidebar'); // Existeix al DOM
$browser->assertNotPresent('#modal');
$browser->assertVisible('.header'); // Visible (no display:none)
$browser->assertMissing('.loading'); // No visible
// Formularis
$browser->assertInputValue('email', 'joan@example.com');
$browser->assertInputValueIsNot('email', '');
$browser->assertChecked('terms');
$browser->assertNotChecked('newsletter');
$browser->assertSelected('country', 'AD');
$browser->assertEnabled('submit');
$browser->assertDisabled('submit');
// Atributs
$browser->assertAttribute('.btn', 'disabled', 'true');
$browser->assertAriaAttribute('.nav', 'expanded', 'true');
$browser->assertDataAttribute('.card', 'id', '42');
// Focus
$browser->assertFocused('email');
$browser->assertNotFocused('password');
// JavaScript i Vue
$browser->assertVue('user.name', 'Joan', '@user-profile');
$browser->assertScript('document.readyState', 'complete');Múltiples navegadors#
Dusk permet obrir múltiples navegadors simultàniament per testejar interaccions entre usuaris. Això és perfecte per a funcionalitats de temps real com xat, col·laboració en temps real o notificacions:
public function test_users_can_chat_in_real_time(): void
{
$joan = User::factory()->create(['name' => 'Joan']);
$maria = User::factory()->create(['name' => 'Maria']);
$this->browse(function (Browser $joanBrowser, Browser $mariaBrowser) use ($joan, $maria) {
// Joan obre el xat
$joanBrowser->loginAs($joan)
->visit('/chat')
->waitFor('#chat-room');
// Maria obre el xat
$mariaBrowser->loginAs($maria)
->visit('/chat')
->waitFor('#chat-room');
// Joan envia un missatge
$joanBrowser->type('#message-input', 'Hola Maria!')
->press('Enviar');
// Maria veu el missatge
$mariaBrowser->waitForText('Hola Maria!')
->assertSeeIn('#chat-messages', 'Joan: Hola Maria!');
});
}Page Objects#
Els Page Objects encapsulen la lògica d'interacció amb una pàgina específica, fent els tests més llegibles i mantenibles. Si l'estructura de la pàgina canvia (un selector CSS, un text de botó), només cal actualitzar el Page Object en lloc de tots els tests que interactuen amb aquella pàgina:
php artisan dusk:page LoginPage// tests/Browser/Pages/LoginPage.php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Page;
class LoginPage extends Page
{
public function url(): string
{
return '/login';
}
public function assert(Browser $browser): void
{
$browser->assertPathIs($this->url())
->assertSee('Iniciar sessió');
}
public function loginWith(Browser $browser, string $email, string $password): void
{
$browser->type('email', $email)
->type('password', $password)
->press('Iniciar sessió');
}
public function elements(): array
{
return [
'@email' => 'input[name=email]',
'@password' => 'input[name=password]',
'@submit' => 'button[type=submit]',
'@error' => '.alert-danger',
];
}
}// Ús en tests
use Tests\Browser\Pages\LoginPage;
public function test_user_can_login(): void
{
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->visit(new LoginPage)
->loginWith($user->email, 'password')
->assertPathIs('/dashboard');
});
}Els selectors definits a elements() amb el prefix @ es poden usar directament als tests: $browser->click('@submit') en lloc de $browser->click('button[type=submit]'). Això fa que els tests siguin més llegibles i que els canvis de selectors es facin en un sol lloc.
Screenshots i depuració#
Dusk fa captures de pantalla automàtiques quan un test falla. Les captures es guarden a tests/Browser/screenshots/ i inclouen el nom del test i un timestamp. Això és extremadament útil per diagnosticar fallades en CI/CD, on no pots veure el navegador:
// Captures manuals durant un test
$browser->screenshot('before-submit');
$browser->screenshot('after-submit');
// Captura amb nom descriptiu
$browser->visit('/dashboard')
->screenshot('dashboard-loaded')
->click('.notification-bell')
->screenshot('notifications-panel-open');Console logs#
Dusk també captura els logs de la consola del navegador. Si hi ha errors de JavaScript, els trobaràs a tests/Browser/console/. Pots accedir-hi programàticament:
$browser->visit('/app')
->assertConsoleLogMissing('Error')
->assertConsoleLogMissing('Uncaught');Depurar amb pause#
El mètode pause() atura el test durant un nombre de mil·lisegons. El mètode stop() atura el test indefinidament fins que premis Enter a la terminal. Això és útil per inspeccionar l'estat del navegador:
// Pausa de 2 segons per veure què passa
$browser->visit('/form')
->type('name', 'Joan')
->pause(2000) // Esperar 2 segons
->press('Enviar');
// Aturar completament per depurar (cal prémer Enter per continuar)
$browser->visit('/dashboard')
->stop(); // El navegador queda obertConfiguració per CI/CD#
Per executar tests de Dusk en un entorn d'integració contínua, cal instal·lar Chrome i configurar Dusk perquè funcioni en mode headless. Un exemple per a GitHub Actions:
# .github/workflows/dusk.yml
name: Dusk Tests
on: [push, pull_request]
jobs:
dusk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install dependencies
run: composer install --no-interaction
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
- name: Setup environment
run: |
cp .env.dusk.ci .env
php artisan key:generate
- name: Run Dusk tests
run: php artisan dusk
- name: Upload screenshots on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: dusk-screenshots
path: tests/Browser/screenshotsL'últim pas (Upload screenshots on failure) puja les captures de pantalla com a artefactes del build, perquè puguis descarregar-les i veure exactament què ha fallat sense necessitat d'executar els tests localment.
Bones pràctiques#
Reduir el nombre de tests de Dusk: els tests de navegador són lents. Utilitza'ls només per a funcionalitats que realment depenen del navegador (JavaScript, interaccions visuals, drag-and-drop). Per a lògica del servidor (validació, CRUD, autenticació), els tests HTTP són més ràpids i més fiables.
Utilitzar selectors estables: els selectors CSS que depenen de classes de disseny (button.bg-blue-500.text-white) són fràgils perquè canvien quan actualitzes l'estil. Prefereix selectors basats en atributs de dades ([data-testid="submit"]), noms (button[name="submit"]) o text (->press('Enviar')).
Esperar, no dormir: mai facis servir pause() per esperar que una acció es completi. Utilitza waitFor(), waitForText() o waitUntil() que s'adapten a la velocitat real de l'aplicació. Un pause(3000) pot ser insuficient en un servidor lent o innecessàriament llarg en un servidor ràpid.
Aïllar els tests: cada test de Dusk hauria de ser independent. No depenguis de l'ordre d'execució ni de dades creades per tests anteriors. Utilitza DatabaseMigrations per netejar la base de dades entre tests i crea totes les dades necessàries dins de cada test.