🎭 Mink

Framework de tests d'acceptation pour applications web

BibliothĂšque PHP pour contrĂŽler des navigateurs web et tester des interactions utilisateur

"Automatisez vos tests fonctionnels comme si un utilisateur réel
naviguait sur votre application"

🎯 Qu'est-ce que Mink ?

Définition

Mink est une couche d'abstraction qui permet d'interagir avec différents navigateurs (réels ou headless) via une API.

Objectif principal

Simuler le comportement d'un utilisateur réel : cliquer sur des liens, remplir des formulaires, naviguer entre les pages, vérifier le contenu affiché.

Cas d'usage

  • Tests d'acceptation (BDD avec Behat)
  • Tests fonctionnels (intĂ©gration Symfony)
  • Tests end-to-end
  • Web scraping

đŸ—ïž Architecture de Mink

Votre Code de Test
↓
Mink (API Unifiée)
↓
Drivers
GoutteDriver ‱ BrowserKit ‱ Selenium2 ‱ ChromeDriver
↓
Navigateurs
Chrome ‱ Firefox
💡 Concept clĂ© : Mink abstrait les diffĂ©rences entre drivers. Vous Ă©crivez votre test une fois, il fonctionne avec n'importe quel driver compatible.

🔌 Les Drivers Mink

Driver Type JavaScript Utilisation
GoutteDriver Headless HTTP ❌ Non Tests rapides, crawling
BrowserKitDriver Symfony Client ❌ Non Tests Symfony internes
Selenium2Driver Navigateur rĂ©el ✅ Oui Tests JS, interactions complexes
ChromeDriver Chrome/Chromium ✅ Oui Tests Chrome headless
⚠ Attention : GoutteDriver et BrowserKitDriver ne supportent pas JavaScript. Utilisez Selenium2Driver ou ChromeDriver pour tester des applications avec beaucoup de JS.
✅ Recommandation : ChromeDriver est idĂ©al pour les tests Chrome/Chromium headless (rapide, moderne, bien maintenu).

📩 Installation avec Symfony

Installation de Mink + ChromeDriver

# Installation de Mink
composer require --dev behat/mink
composer require --dev behat/mink-goutte-driver
composer require --dev behat/mink-browserkit-driver
composer require --dev behat/mink-selenium2-driver

# Installation de ChromeDriver
composer require --dev dmore/chrome-mink-driver

# Télécharger le binaire ChromeDriver depuis:
# https://chromedriver.chromium.org/

Configuration avec ChromeDriver

use Behat\Mink\Mink;
use Behat\Mink\Session;
use DMore\ChromeDriver\ChromeDriver;

// Configuration de la session ChromeDriver
$mink = new Mink([
    'chrome' => new Session(
        new ChromeDriver('http://localhost:9222', null, 'http://localhost')
    ),
]);

$mink->setDefaultSessionName('chrome');
$session = $mink->getSession();
$session->start();

// Votre test
$session->visit('https://example.com');
$page = $session->getPage();
$page->findButton('Submit')->click();
💡 Astuce : DĂ©marrez Chrome en mode headless avec : chrome --headless --remote-debugging-port=9222

🔑 API de Base - Session & Navigation

Session
Navigation
use Behat\Mink\Mink;
use Behat\Mink\Session;
use Behat\Mink\Driver\GoutteDriver;

// Création de la session
$mink = new Mink([
    'goutte' => new Session(new GoutteDriver())
]);

// Définir la session par défaut
$mink->setDefaultSessionName('goutte');

// Récupérer la session
$session = $mink->getSession();

// Démarrer la session
$session->start();

// ArrĂȘter la session
$session->stop();
// Visiter une URL
$session->visit('https://example.com');

// Récupérer l'URL actuelle
$currentUrl = $session->getCurrentUrl();

// Recharger la page
$session->reload();

// Navigation historique
$session->back();
$session->forward();

// Récupérer la page courante
$page = $session->getPage();

// Récupérer le code de statut HTTP
$statusCode = $session->getStatusCode();

// Gérer les cookies
$session->setCookie('name', 'value');
$cookie = $session->getCookie('name');

🎯 SĂ©lecteurs et Recherche d'ÉlĂ©ments

CSS
XPath
Nommés
// Sélecteur CSS (le plus courant)
$page = $session->getPage();

// Trouver un élément unique
$element = $page->find('css', '.my-class');
$button = $page->find('css', '#submit-btn');
$link = $page->find('css', 'a.menu-link');

// Trouver plusieurs éléments
$items = $page->findAll('css', 'ul.products li');

// Vérifier l'existence
if ($element) {
    $text = $element->getText();
    $html = $element->getHtml();
}
// Sélecteur XPath (plus puissant)

// Trouver par texte exact
$element = $page->find('xpath', '//button[text()="Envoyer"]');

// Trouver par texte partiel
$element = $page->find('xpath', '//a[contains(text(), "Lire")]');

// Trouver par attribut
$input = $page->find('xpath', '//input[@name="email"]');

// Combinaisons complexes
$element = $page->find(
    'xpath', 
    '//div[@class="card"]//h2[contains(text(), "Titre")]'
);
// Sélecteurs nommés (plus lisibles)

// Trouver un lien par son texte
$link = $page->findLink('Connexion');

// Trouver un bouton par son texte ou ID
$button = $page->findButton('Valider');

// Trouver un champ de formulaire
$field = $page->findField('email');
$field = $page->findField('Votre email'); // Par label

// Trouver par ID
$element = $page->findById('my-element');
✅ Bonne pratique : Utilisez les sĂ©lecteurs nommĂ©s quand c'est possible (plus lisible), CSS pour les sĂ©lections simples, XPath pour les cas complexes.

đŸ–±ïž Interactions avec les ÉlĂ©ments

Clics & Liens
Formulaires
Avancé
// Cliquer sur un élément
$button = $page->findButton('Valider');
$button->click();

// Cliquer sur un lien
$link = $page->findLink('En savoir plus');
$link->click();

// Méthode raccourcie
$page->clickLink('Connexion');

// Double-clic (nécessite un driver JS)
$element = $page->find('css', '.item');
$element->doubleClick();

// Clic droit
$element->rightClick();
// Remplir un champ texte
$page->fillField('email', '[email protected]');
$page->fillField('password', 'secret123');

// Cocher une checkbox
$page->checkField('terms');

// Décocher une checkbox
$page->uncheckField('newsletter');

// Sélectionner dans un select
$page->selectFieldOption('country', 'France');

// Select multiple
$page->selectFieldOption('colors', ['red', 'blue']);

// Soumettre un formulaire
$page->pressButton('Envoyer');
// Upload de fichier
$field = $page->findField('avatar');
$field->attachFile('/path/to/file.jpg');

// Hover (survol souris - nécessite driver JS)
$element = $page->find('css', '.menu-item');
$element->mouseOver();

// Focus sur un élément
$element->focus();

// Appuyer sur une touche
$element->keyPress('Enter');
$element->keyDown('Shift');
$element->keyUp('Shift');

// Drag & Drop (nécessite driver JS)
$source = $page->find('css', '.draggable');
$target = $page->find('css', '.droppable');
$source->dragTo($target);

✅ VĂ©rifications et Assertions

Contenu
ÉlĂ©ments
Formulaires
// Vérifier le contenu de la page
$page = $session->getPage();

// Vérifier la présence de texte
if ($page->hasContent('Bienvenue')) {
    // Le texte est présent
}

// Assertions avec WebAssert (recommandé)
use Behat\Mink\WebAssert;

$assert = new WebAssert($session);

// Vérifier le code HTTP
$assert->statusCodeEquals(200);

// Vérifier la présence de texte
$assert->pageTextContains('Connexion réussie');
$assert->pageTextNotContains('Erreur');

// Vérifier l'URL
$assert->addressEquals('/dashboard');
$assert->addressMatches('/\/user\/\d+/');
// Vérifier la présence d'éléments
$assert->elementExists('css', '.alert-success');
$assert->elementNotExists('css', '.alert-danger');

// Compter les éléments
$assert->elementsCount('css', 'ul.products li', 10);

// Vérifier le texte d'un élément
$assert->elementContains('css', 'h1', 'Titre principal');
$assert->elementTextContains('css', '.price', '€');

// Vérifier les attributs
$assert->elementAttributeContains('css', 'a.active', 'href', '/home');

// Vérifier la visibilité
$element = $page->find('css', '.modal');
if ($element->isVisible()) {
    // L'élément est visible
}
// Vérifier les champs de formulaire
$assert->fieldExists('email');
$assert->fieldNotExists('fake-field');

// Vérifier la valeur d'un champ
$assert->fieldValueEquals('email', '[email protected]');
$assert->fieldValueNotEquals('password', '');

// Vérifier qu'une checkbox est cochée
$assert->checkboxChecked('terms');
$assert->checkboxNotChecked('newsletter');

// Vérifier les boutons
$assert->buttonExists('Valider');

// Vérifier les liens
$assert->linkExists('Mot de passe oublié ?');
✅ Bonne pratique : Utilisez WebAssert pour des messages d'erreur clairs et des vĂ©rifications robustes.

⏱ JavaScript & Gestion de l'Attente

⚠ Important : Les fonctionnalitĂ©s JavaScript nĂ©cessitent un driver compatible (Selenium2Driver, ChromeDriver). GoutteDriver et BrowserKitDriver ne les supportent pas.
Attente
Exécuter JS
AJAX
// Attendre qu'un élément soit visible
$page->waitFor(5000, function ($page) {
    return $page->find('css', '.loaded')->isVisible();
});

// Attendre qu'un élément apparaisse
$session->wait(5000, "document.querySelector('.modal') !== null");

// Attendre la fin d'une animation
$session->wait(3000, "jQuery.active == 0");

// Attendre avec timeout personnalisé
$session->wait(10000, "document.readyState === 'complete'");
// Exécuter du JavaScript
$result = $session->evaluateScript('return document.title;');

// Exécuter sans retour
$session->executeScript('window.scrollTo(0, document.body.scrollHeight);');

// Manipuler le DOM
$session->executeScript("
    document.querySelector('.element').style.display = 'block';
");

// Déclencher des événements
$session->executeScript("
    var event = new Event('change');
    document.querySelector('#my-input').dispatchEvent(event);
");

// Récupérer des données
$data = $session->evaluateScript('
    return JSON.stringify({
        url: window.location.href,
        cookies: document.cookie
    });
');
// GĂ©rer les requĂȘtes AJAX

// Cliquer et attendre la réponse AJAX
$button = $page->findButton('Charger plus');
$button->click();

// Attendre que jQuery termine
$session->wait(5000, 'jQuery.active == 0');

// Ou attendre l'apparition du nouveau contenu
$page->waitFor(5000, function ($page) {
    $items = $page->findAll('css', '.product-item');
    return count($items) > 10;
});

// VĂ©rifier qu'une requĂȘte AJAX s'est terminĂ©e
$session->wait(5000, "
    typeof window.ajaxComplete !== 'undefined' && window.ajaxComplete === true
");

đŸ’» Exemple Complet : Test de Connexion

use Behat\Mink\Mink;
use Behat\Mink\Session;
use DMore\ChromeDriver\ChromeDriver;
use Behat\Mink\WebAssert;

class LoginTest extends TestCase
{
    private Mink $mink;
    private Session $session;
    
    protected function setUp(): void
    {
        // Configuration ChromeDriver
        $this->mink = new Mink([
            'chrome' => new Session(
                new ChromeDriver('http://localhost:9222', null, 'http://localhost')
            ),
        ]);
        
        $this->mink->setDefaultSessionName('chrome');
        $this->session = $this->mink->getSession();
        $this->session->start();
    }

    public function testSuccessfulLogin(): void
    {
        // AccĂšde Ă  la page de connexion
        $this->session->visit('http://localhost/login');
        $page = $this->session->getPage();
        
        // Vérifie qu'on est sur la bonne page
        $assert = new WebAssert($this->session);
        $assert->elementTextContains('css', 'h1', 'Connexion');
        
        // Remplit le formulaire
        $page->fillField('_username', '[email protected]');
        $page->fillField('_password', 'password123');
        $page->checkField('_remember_me');
        
        // Soumet le formulaire
        $page->pressButton('Se connecter');
        
        // Attend la redirection
        $this->session->wait(5000, "document.querySelector('.dashboard') !== null");
        
        // Vérifie qu'on est bien connecté
        $assert->elementExists('css', '.user-menu');
        $assert->elementTextContains('css', '.welcome-message', 'Bienvenue');
        
        // Vérifie l'URL
        $assert->addressMatches('/\/dashboard/');
    }
    
    public function testFailedLogin(): void
    {
        $this->session->visit('http://localhost/login');
        $page = $this->session->getPage();
        $assert = new WebAssert($this->session);
        
        // Remplit avec de mauvais identifiants
        $page->fillField('_username', '[email protected]');
        $page->fillField('_password', 'wrongpassword');
        $page->pressButton('Se connecter');
        
        // Attend et vérifie le message d'erreur
        $this->session->wait(5000, "document.querySelector('.alert-danger') !== null");
        $assert->elementTextContains('css', '.alert-danger', 'Identifiants invalides');
        
        // Vérifie qu'on est toujours sur /login
        $assert->addressMatches('/\/login/');
    }
    
    protected function tearDown(): void
    {
        $this->session->stop();
    }
}

✹ Bonnes Pratiques

1. Utilisez des sélecteurs stables - Préférez les data-attributes (data-test-id) aux classes CSS qui peuvent changer.
2. Gérez les timeouts - Ajoutez des attentes explicites pour le contenu dynamique (AJAX, animations).
3. Isolez vos tests - Chaque test doit ĂȘtre indĂ©pendant et rĂ©initialiser l'Ă©tat de la base de donnĂ©es.
4. Testez le comportement, pas l'implémentation - Concentrez-vous sur ce que l'utilisateur voit et fait.
5. Utilisez WebAssert - Messages d'erreur clairs et vérifications robustes.
6. Headless pour la CI/CD - Utilisez des navigateurs headless (ChromeDriver) en intégration continue.
7. Screenshots sur échec - Capturez des screenshots quand un test échoue pour faciliter le debug.
8. Évitez les sleep() - PrĂ©fĂ©rez les attentes conditionnelles (waitFor) aux sleep() arbitraires.

🎓 Astuces AvancĂ©es

1. Screenshots et Debug

// Capturer un screenshot (avec Selenium2Driver ou ChromeDriver)
$screenshot = $session->getDriver()->getScreenshot();
file_put_contents('/path/to/screenshot.png', base64_decode($screenshot));

// Screenshot sur échec de test
protected function onNotSuccessfulTest(Throwable $t): void
{
    if ($this->session) {
        $screenshot = $this->session->getDriver()->getScreenshot();
        file_put_contents(
            sprintf('/tmp/test-failure-%s.png', date('Y-m-d-H-i-s')),
            base64_decode($screenshot)
        );
    }
    throw $t;
}

2. GĂ©rer plusieurs fenĂȘtres/onglets

// RĂ©cupĂ©rer tous les noms de fenĂȘtres
$windowNames = $session->getWindowNames();

// Changer de fenĂȘtre
$session->switchToWindow($windowNames[1]);

// Ouvrir un lien dans un nouvel onglet puis switcher
$link = $page->findLink('Ouvrir');
$link->click();
$session->switchToWindow($session->getWindowNames()[1]);

3. Gérer les alertes/confirmations JavaScript

// Accepter une alerte
$session->getDriver()->getWebDriverSession()->accept_alert();

// Annuler une confirmation
$session->getDriver()->getWebDriverSession()->dismiss_alert();

// Récupérer le texte de l'alerte
$alertText = $session->getDriver()->getWebDriverSession()->getAlert_text();

🎯 Conclusion

Mink est l'outil idéal pour automatiser vos tests d'acceptation
et garantir la qualité de vos applications Symfony

Points clés à retenir :

  • ✅ API unifiĂ©e pour tous les drivers
  • ✅ ChromeDriver recommandĂ© pour tests modernes avec JS
  • ✅ Support JavaScript avec les bons drivers
  • ✅ WebAssert pour des assertions claires
  • ✅ Tests end-to-end comme un utilisateur rĂ©el
🚀 Prochaines Ă©tapes : Installez ChromeDriver, configurez votre environnement de test, Ă©crivez votre premier test, intĂ©grez dans votre CI/CD, et automatisez vos scĂ©narios utilisateur !
Ressources :
📚 Documentation officielle Mink : mink.behat.org
🚗 ChromeDriver : chromedriver.chromium.org
📩 Package PHP : github.com/dmore/chrome-mink-driver
1 / 14