De Selenium à Panther !
Que sont les tests de bout-en-bout ?
Les tests de bout-en-bout ou tests end-to-end (vous trouverez aussi cette notation : E2E) permettent de valider les scénarios métiers d’une application de manière automatique. Pour cela, nous simulons les différentes interactions que peut avoir un utilisateur au travers du navigateur et nous nous assurons ainsi que le comportement de l’application est bien celui qui est attendu. C’est une pratique régulière que nous utilisons lors de la création d’applications web ou d’applications métier.
Rappel – les tests dans Symfony
Symfony nous met à disposition plusieurs outils afin de réaliser différents types de tests :
Les Tests unitaires
Symfony propose une version améliorée de PHPUnit appelé « Simple PHPUnit » disponible avec l’extension Bridge PHPUnit.
Cette extension va nous permettre d’être averti de l’obsolescence de nos tests ou de l’exécution de code déprécié, ainsi que nous aider à simuler des fonctions natives liées au temps, au DNS et à l’existence des classes.
Les Tests fonctionnels
Ces tests vérifient l’intégration de différentes couches de l’application. Ils suivent un cycle de vie spécifique :
- Effectuer une requête auprès de l’application ;
- Parcourir le DOM (le code HTML de la page) ;
- Contrôler la réponse du serveur.
Afin de montrer l’intérêt des tests de bout-en-bout, nous allons dans un premier temps tenter de comprendre comment fonctionne les tests fonctionnels dans Symfony.
Comprendre les tests fonctionnels dans Symfony
Symfony utilise le composant BrowserKit construit autour des composants DomCrawler et CssSelector.
Le composant BrowserKit :
Simule le comportement d’un navigateur web, ce qui permet d’effectuer des requêtes, de cliquer sur des liens et de soumettre des formulaires de manière programmatique.
Le composant DomCrawler :
Facilite la navigation dans le DOM pour les documents HTML et XML.
Le composant CssSelector :
Convertit les sélecteurs CSS en expressions XPath.
Nos tests fonctionnels sont réalisés à partir de la classe WebTestCase.
Cette classe étend la classe KernelTestCase qui elle-même étend de la classe TestCase de PHPUnit.
Le rôle de ces classes est le suivant :
La classe KernelTestCase boot l’application Symfony et permet ainsi d’accéder au kernel de symfony.
La classe WebTestCase utilise le composant BrowserKit pour simuler un navigateur. Elle permet de créer une requête HTTP (HttpFoundation) et d’inspecter la réponse au travers d’une API.
⚠️ Il n’y a aucun appel HTTP !
Exemple pour mettre en illustration ce type de test.
Nous prendrons une application de blog simple et minimaliste avec ajout/modification/suppression d’article.
class ArticleTest extends WebTestCase {
public function testCreateArticle(): void {
$client = static::createClient(); $client->request('GET', '/article/'); $client->followRedirects();
$this->assertPageTitleSame('Articles'); $client->clickLink('Create new');
$this->assertPageTitleSame('New Article'); $this->assertSelectorTextSame('h1', 'Create new Article');
$client->submitForm('Save', [ 'article[title]' => 'Symfony: les tests de bout-en-bout', 'article[description]' => 'Cet article décrit les tests de bout-en-bout avec Symfony', 'article[content]' => 'We love Panther !' ]);
$this->assertSelectorTextContains('table', 'Symfony: les tests de bout-en-bout'); } }
Que fait ce test ?
- Nous créons un client puis effectuons une requête de type GET sur l’uri /article/ à partir de ce client
- Nous indiquons à ce client de suivre les redirections (nécessaire lors de la soumission du formulaire)
- Première assertion. Nous vérifions que la balise <title> contient le mot « Articles »
- Le client clique sur le lien dont le texte est « Create new »
- Deuxième assertion. Nous vérifions que la balise <title> contient le mot « New Article »
- Troisième assertion. Nous vérifions que la première balise <h1> contient le mot « Create New Article ».
- Le client soumet le formulaire avec des données.
- Quatrième assertion. Nous sommes revenus à la page d’accueil (redirection effectuée après la soumission du formulaire). Nous vérifions que la balise <table> contient le texte « Symfony : les tests de bout-en-bout ».
Toutes ces assertions nous sont fournis par la classe WebTestCase.
Voici ce que cela donnerait côté navigateur :
Notre page d’accueil contenant la liste des articles (vide pour l’instant)
Le formulaire de création d’un article
Notre page d’accueil contenant la liste de nos articles
Résultat de notre test ?
Il est validé !
Mais nous avons un problème…
Imaginons que nous rencontrons un bug.
Nous allons le simuler en rendant notre formulaire invisible avec la règle css suivante :
#article {
visibility : hidden ;
}
Notre formulaire ressemblerait alors à cela :
Réexécutons notre test
Il est validé alors que notre formulaire n’est plus visible !
C’est tout à fait normal.
Nous effectuons des assertions sur la réponse retournée par le kernel de Symfony.
On peut ainsi parcourir cet objet et simuler des interactions.
En revanche, ni le javascript, ni le css n’est interprété. Par conséquent, notre formulaire « existe » toujours « aux yeux » de notre objet.
Pour détecter ce type d’erreur, nous devons effectuer nos tests au travers d’un vrai navigateur.
Les tests de bout-en-bout
Nous allons tester deux solutions dans le but de résoudre le problème que nous avons constaté lors de notre test fonctionnel.
Première solution : Selenium
Selenium est une suite d’outils permettant de simuler des interactions avec des navigateurs Web. Il fournit les outils suivants :
- Selenium WebDriver : fournit une API permettant d’interagir avec un navigateur web de manière programmatique.
- Selenium IDE : plugin pour navigateur web permettant d’enregistrer des scénarios de tests à partir des différentes actions faites par l’utilisateur.
- Selenium Grid : serveur permettant d’exécuter les tests.
Nous n’utiliserons pas Selenium WebDriver car notre langage préféré n’est pas supporté.
Nous n’utiliserons pas non plus Selenium IDE car l’export des tests réalisés à partir de cette interface n’est pas compatible avec PHPUnit.
Nous utiliserons en revanche Selenium Grid mais de manière standalone.
Enfin nous utiliserons le navigateur Firefox pour exécuter nos tests.
Mise en place
- Afin de faire fonctionner notre serveur Selenium, nous avons besoin de java.
- Nous devons ensuite récupérer l’exécutable du serveur Selenium à l’adresse https://www.selenium.dev/downloads/
- Nous devons aussi récupérer le WebDriver de Firefox afin d’interagir avec notre navigateur. Nous l’installons directement via apt.
- Afin de communiquer avec notre WebDriver, nous installons la librairie php-webdriver.
Écriture de notre test
- Les fonctions setup() et tearDown()
La fonction setup() nous sert à initialiser le driver avec ces différentes options dans le but de se connecter au serveur Selenium.
Par défaut, le serveur Selenium écoute sur le port 4444.
protected RemoteWebDriver $driver;
public function setup(): void
{
$serverUrl = 'http://localhost:4444';
$desiredCapabilities = DesiredCapabilities::firefox();
$desiredCapabilities->setPlatform(WebDriverPlatform::LINUX);
$desiredCapabilities->setCapability('acceptSslCerts', false);
$firefoxOptions = new FirefoxOptions();
$firefoxOptions->addArguments(['-headless']);
$desiredCapabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions);
$this->driver = RemoteWebDriver::create($serverUrl, $desiredCapabilities);
}
La fonction tearDown() nous permet de terminer la session avec le navigateur.
public function tearDown(): void
{
$this->driver->quit();
}
Notre test :
C’est exactement la même logique que notre test fonctionnel.
public function testCreateArticle(): void
{
$this->driver->get("http://localhost:8000/article");
$this->assertEquals("Articles", $this->driver->getTitle());
$btnCreate = $this->driver->findElement(WebDriverBy::linkText('Create new'));
$btnCreate->click();
$this->assertEquals("New Article", $this->driver->getTitle());
$h1 = $this->driver->findElement(WebDriverBy::cssSelector('main div.container div.row h1'));
$this->assertEquals('Create new Article', $h1->getText());
$this->driver
->findElement(WebDriverBy::name('article[title]'))
->sendKeys('Symfony: les tests de bout-en-bout');
$this->driver
->findElement(WebDriverBy::name('article[description]'))
->sendKeys('Cet article décrit les tests de bout-en-bout avec Symfony');
$this->driver
->findElement(WebDriverBy::name('article[content]'))
->sendKeys('We love Panther !')
->submit();
$this->driver
->wait()
->until(WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::cssSelector('.table')));
$table = $this->driver->findElement(WebDriverBy::cssSelector('.table'));
$this->assertStringContainsString('Symfony: les tests de bout-en-bout', $table->getText());
}
Exécutons à présent notre serveur Selenium en mode standalone.
Exécutons notre test
Nous avons bien une erreur !
Notre élément dont le nom est « article[title] » n’est pas accessible.
Corrigeons notre erreur et réexécutons notre test !
Il est bien validé !
Vous l’avez peut-être remarqué, il y a une grande différence au niveau des temps d’exécutions.
Nous sommes à 12 secondes contre 300ms avec notre test fonctionnel.
Deuxième solution : Panther
Panther est une bibliothèque autonome servant à exécuter des tests de bout-en-bout en utilisant de vrais navigateurs.
Il met en œuvre les API des composants BrowserKit et DomCrawler de Symfony.
Enfin, il démarre automatiquement notre application en utilisant le serveur web intégré de PHP.
Mise en place
- Nous devons récupérer le WebDriver de Firefox afin d’interagir avec notre navigateur.
Nous l’avons déjà fait lors de la mise en place de notre environnement avec Selenium.
- Nous devons installer Panther.
Écriture de notre test
class ArticlePantherTest extends PantherTestCase
{
public function testCreateArticle(): void
{
$client = static::createPantherClient(['browser' => static::FIREFOX]);
$client->request('GET', '/article/');
$client->followRedirects();
$this->assertPageTitleSame('Articles');
$client->clickLink('Create new');
$this->assertPageTitleSame('New Article');
$this->assertSelectorTextSame('h1', 'Create new Article');
$client->submitForm('Save', [
'article[title]' => 'Symfony: les tests de bout-en-bout',
'article[description]' => 'Cet article décrit les tests de bout-en-bout avec Symfony',
'article[content]' => 'We love Panther !'
]);
$this->assertSelectorTextContains('table', 'Symfony: les tests de bout-en-bout');
}
Incroyable ! Les seuls éléments qui ont changé par rapport à l’écriture de notre test fonctionnel, sont la classe PantherTestCase et la création du client !
Exécutons notre test
Parfait ! Nous détectons également la même une erreur !
Notre élément dont le nom est « article[title] » n’est pas accessible.
Par défaut, Panther enregistre également un screenshot de la page en erreur dans le répertoire /var/error-screenshots
Nous voyons bien que le formulaire est invisible.
Corrigeons notre erreur et réexécutons notre test !
Il est bien validé !
Nous remarquons qu’il est plus rapide que le test effectué avec le serveur Selenium mais cela reste néanmoins élevé.
Ajoutons des commentaires à nos articles
Nous allons ajouter un système de commentaire en Javascript.
Pour cela :
- Nous créons une entité Comment avec le maker de Symfony
- Nous installons API Platform afin d’exposer très rapidement une API
- Nous utilisons Webpack Encore pour la partie Javascript.
Notre système de commentaire ressemble à cela :
Après soumission du formulaire.
Écriture de notre test
public function testComment(): void
{
$client = static::createPantherClient(['browser' => static::FIREFOX]);
$client->request('GET', '/article/1');
$client->waitFor('#post-comment');
$client->submitForm('Post', [
'body' => 'We love!',
'author' => 'Sooyoos',
]);
$client->waitFor('#status.active');
$this->assertSelectorTextContains('#comments', 'We love!');
}
$client->waitFor('#post-comment');
Le « client » attend que l’élément avec l’id post-comment soit chargé au niveau du DOM (Cela correspond à notre formulaire)
$client->waitFor('#status.active');
Le client attend que l’élément avec l’id status et la classe active soit chargé au niveau du DOM (Cela correspond à notre « Comment published ! » qui apparaît après l’ajout du commentaire).
Exécutons notre test
Parfait ! Nous avons pu tester des éléments du DOM écrit en Javascript !
Conclusion
Nous venons de voir au travers d’un exemple très simple la mise en place de tests de bout-en-bout sur une application Symfony.
Ils sont très utiles afin de tester des scénarios métiers dit « critique« . En effet, au vu de leur temps d’exécution (entre 8 et 12 secondes pour notre simple test), il serait peut-être préférable de les limiter !
L’utilisation de Selenium ajoute de la complexité à notre application où, Panther, nous la simplifie. En effet, la mise en place d’un serveur Selenium nécessite l’installation de Java Runtime alors que Panther utilise directement le serveur web intégré de PHP.
De plus, nous devons utiliser une nouvelle librairie (php-webdriver) pour écrire nos tests alors que Panther est compatible avec les composants utilisés pour les tests fonctionnels dans Symfony. Nous retrouvons ainsi une certaine familiarité.
Enfin, pour aller plus loin, nous pourrions intégrer nos tests dans un processus d’intégration continue en conteneurisant nos différents services ou encore utiliser des outils tel que BrowserStack afin d’exécuter nos tests sur plusieurs devices et navigateurs (Cela pourrait peut-être faire l’objet d’un prochain article ?)
Sources utilisées pour rédiger cet article :
- https://github.com/symfony/panther
- https://www.youtube.com/watch?v=3A8yaRicrVk
- https://www.selenium.dev
- https://symfony.com