Il y a 9 années · 10 minutes · Java / JEE

Optimisez les performances de vos tests fonctionnels avec Selenium Grid

Lorsque l’on fait de l’intégration continue, il est nécessaire que les temps d’exécution des builds et tests soient les plus courts possible pour que les équipes aient un feedback rapide sur les résultats. Or, lorsqu’une application devient riche en fonctionnalités, le nombre de tests augmente et le temps d’exécution de ceux-ci devient non négligeable. Ceci est plus particulièrement vrai pour les tests fonctionnels, et notamment ceux qui testent les interfaces utilisateurs (UI) car ils s’exécutent dans un navigateur web et donc subissent une certaine latence dû justement à l’utilisation du navigateur.

Selenium s’est aujourd’hui imposé comme l’une des solutions open-source et gratuite les plus efficaces pour effectuer des tests fonctionnels des interfaces utilisateurs d’applications web. Son utilisation par défaut permet d’exécuter les tests de manière séquentielle, ce qui entraîne une augmentation linéaire du temps global d’exécution de l’ensemble des tests à mesure qu’on en ajoute. Heureusement, comme nous allons le voir dans cet article, une solution existe : Selenium Grid. Cette extension de Selenium, permet d’exécuter en parallèle les tests fonctionnels et ainsi réduire considérablement le temps global d’exécution de ceux-ci.

Nous ne rentrerons pas ici dans le détail de l’utilisation de Selenium (qui est très bien documenté sur le site officiel) hormis un exemple simple, mais nous regarderons plutôt comment paralléliser les tests avec Selenium Grid.

Selenium

Présentation

Selenium est un outil d’automatisation de tests fonctionnels permettant d’exécuter des scénarios d’interactions utilisateur avec une application web. Il permet d’une part de valider les fonctionnalités de l’application, et d’autre part de tester sa compatibilité avec des environnements client hétérogènes (browser et OS sur lequel est utilisé le browser).

Selenium regroupe plusieurs outils :

  • Selenium IDE : Ce plugin (uniquement disponible pour FireFox), permet d’enregistrer et de rejouer des séquences d’interactions entre l’utilisateur et l’application via le navigateur web et de les exporter vers différents langages (Java, .Net, Php, …)
  • Selenium Core : Cette librairie Javascript permet à Selenium d’exécuter les commandes enregistrées via l’outil IDE dans le navigateur. Elle permet également à des non-développeurs d’exécuter les tests en utilisant le HTML.
  • Selenium Remote Control : Le Remote Control Server, un serveur http léger (Jetty) associé a une des librairies clientes disponibles pour divers langages de programmation (Java, .Net, ..) permet d’exécuter les tests codés ou enregistrés à l’aide de Selenium IDE.
  • Selenium Grid : Le Selenium Grid server permet de piloter et de distribuer à une série d’instances Selenium RC les requêtes et ainsi exécuter en parallèle les tests sur plusieurs instances de navigateurs divers, éventuellement sur des machines différentes et dans des environnements différents (OS).

Un exemple de test Selenium intégré à JUnit

Voici un exemple de classe regroupant une suite de tests UI d’une application Web :


import junit.framework.Test;
import junit.framework.TestSuite;

public class UIAllTests {

    public static Test suite() {
        TestSuite ts = new TestSuite();
               
        ts.addTestSuite(ViewCustomerModelUITest.class);
        ts.addTest(ProjectManagementUITests.suite());
        ts.addTest(UserManagementUITests.suite());
        ...

        return ts;
    }
}

La classe ViewCustomerModelUITest exécute le scénario de recherche d’un modèle produit d’un compte client :

  • L’utilisateur s’authentifie et se connecte à l’application (login)
  • Clic sur le lien « Accounts »
  • Clic sur le lien « View Customer Models »
  • Saisie du mot « ford » dans le champs « freeText »
  • Clic sur le bouton « submit » pour lancer la recherche
  • Déconnexion de l’application (logout)
import com.thoughtworks.selenium.*;

public class ViewCustomerModelUITest extends BaseSeleniumTestCase {

    public ViewCustomerModelUITest() {
        super();
    }
        
    public void setUp() throws Exception {
        super.setUp();
    }
     
    public void testViewCustomerModelUI() throws Exception {
        login(MyAppConst.ADMIN_CREDENTIALS);
        selenium.click("link=Accounts");
        selenium.waitForPageToLoad("30000");
        selenium.click("link=> View Customer Models");
        selenium.waitForPageToLoad("30000");
        selenium.type("freeText", "ford");
        selenium.click("submit");
        selenium.waitForPageToLoad("30000");
        logout();
    }

L’implémentation des fonctions communes (login, logout, ouverture d’un nouveau navigateur) aux classes de test Selenium se trouve dans la classe BaseSeleniumTestCase

public class BaseSeleniumTestCase {
   
    protected Selenium selenium;

    public BaseSeleniumTestCase () {
        super();
    }

    // créé une nouvelle instance de Selenium et ouvre un browser
    protected void setUp ()
        throws Exception {
        super.setUp();
        selenium =
            new DefaultSelenium(
                "localhost",
                "9999",
                "*chrome",
                "http://localhost:8080/myapp/");
    }

    // authentifie l'utilisateur sur le site
    protected void login (String[] credentials) {
        selenium.open("http://localhost:8080/myapp/");
        selenium.type("j_username", credentials[0]);
        selenium.type("j_password", credentials[1]);
        selenium.click("submit");
        selenium.waitForPageToLoad("30000");
    }

    // déconnecte l'utilisateur du site
    protected void logout ()
        throws Exception {
        selenium.click("link=Log out");
        selenium.waitForPageToLoad("30000");
    }

    // ferme le navigateur
    protected void tearDown ()
        throws Exception {
        super.tearDown();
        selenium.stop();
    }
}

On remarquera ici qu’un nouveau navigateur est utilisé à l’exécution de chaque test (ou pour les tests regroupés dans la même classe). Si on utilise Selenium standard en mode séquentiel, il serait plus judicieux d’utiliser la même instance Selenium (le même browser) et d’exécuter les tests avec celle-ci (en utilisant par exemple une classe singleton permettant aux classes tests de récupérer l’unique instance). Mais comme nous allons le voir, avec la parallélisation des tests et Selenium Grid ce n’est plus nécessaire.

Parallélisation des tests

Jusqu’ici nous avons vu un exemple de classes TestSuite Junit classique qui exécute les tests de manière séquentielle. Pour pouvoir utiliser Selenium Grid et profiter des avantages du mode d’exécution parallèle, il est obligatoire que les tests Junit (et par extension ceux écrits pour Selenium) soient exécutés eux-mêmes en parallèle, ce qui implique en Java l’utilisation de threads concurrents.

Il est important ici de comprendre que l’exécution parallèle des tests unitaires requiert que ceux-ci ne soient pas dépendants les uns des autres. Ainsi, des scénarios de tests (implémentés avec les {{TestSuite}}) sont censés être isolés et donc exécutables de manière concurrente. Il faut donc s’assurer que les tests unitaires sont bien regroupés par cas d’utilisation dans des classes TestSuite différents, et ce, plus particulièrement, lorsque l’on a des tests qui effectuent des opérations de type CRUD sur la base de données.

Junit ne permet pas nativement d’exécuter des tests unitaires dans des threads différents (on peut le constater dans cet exemple pratique ici).

Plusieurs méthodes existent pour écrire des tests multi-threadés exécutés avec Junit : par exemple avec GroboUtils (un exemple ici) qui rend la classe TestRunner multi-tâches ({{MultiThreadedTestRunner}}). Mais cela suppose que les tests n’ont pas encore été écrits, et que l’on désire tester explicitement les accès concurrents à une application.

Dans l’exemple qui va suivre, nous allons partir de l’hypothèse que l’on a déjà écrit nos tests unitaires de manière linéaire et séquentielle et que l’on veut simplement les paralléliser. Si on utilise JUnit, il existe une solution rapide et efficace : ParallelJUnit. Et pour ceux qui utilisent le framework plus récent TestNG, la solution est quasi immédiate.

ParallelJUnit

ParallelJUnit est une librairie Java implémentant la classe ParallelTestSuite qui étend la classe junit.framework.TestSuite pour la rendre multi-threadée. Le principe est simple : tous les tests ou sous-suites de tests qui sont référencés dans une instance de la classe ParallelTestSuite seront exécutés dans des threads différents.

Si on reprend notre exemple de tests junit précédent on aura alors le code suivant :


import junit.framework.Test;
import junit.framework.TestSuite;

import org.kohsuke.junit.ParallelTestSuite;

public class UIAllTests {

    public static Test suite() {
        TestSuite ts = new ParallelTestSuite();
               
        ts.addTestSuite(ViewCustomerModelUITest.class);
        ts.addTest(ProjectManagementUITests.suite());
        ts.addTest(UserManagementUITests.suite());
        ...

        return ts;
    }
}

Ainsi, les tests ViewCustomerModelUITest, ProjectManagementUITests et UserManagementUITests seront exécutés dans des threads parallèles différents. En revanche, les suites de tests et les tests contenus dans les classes ProjectManagementUITests et UserManagementUITests seront toujours exécutés de manière séquentielle, sauf si ces classes utilisent de la même manière l’implémentation ParallelTestSuite à la place de TestSuite.

TestNG

Au contraire de Junit, le framework TestNG implémente nativement le multi-threading. Si on se réfère à la documentation, on peut configurer le multi-threading :
– soit dans le tag de la tâche Ant en spécifiant le scope (méthodes ou classes de tests) et le nombre de threads concurrents :


– soit dans le code Java directement en utilisant les annotations :

@Test(threadPoolSize = 3, invocationCount = 10,  timeOut = 10000)
public void testServer() {
   ...
}

Comme on peut le constater, la parallélisation des tests est également très simple avec TestNG et permet d’utiliser les annotations (pour les fans).

Voilà, la parallélisation de nos tests est terminée, on peut désormais utiliser toute la puissance de Selenium Grid !

Selenium Grid

Mise en place

Ceux qui ont déjà utilisé Selenium ont pu constater que, en dehors de la partie codage des tests Selenium, la configuration et l’utilisation du serveur Selenium RC pour l’exécution des tests est vraiment très aisée. Et bien pour Selenium Grid c’est exactement la même chose !

Comme on peut le voir sur le schéma ci-après, Selenium Grid Server fait office de « hub », s’intégrant dans une configuration Selenium classique et permet de load-balancer les requêtes lancées par les tests vers les différents navigateurs référencés. Le « hub » redirige automatiquement les requêtes vers le Selenium RC correspondant à l’environnement cible demandé.

Maintenant, regardons de plus près comment mettre tout ça en marche dans la pratique avec Ant. On va d’abord configurer notre hub Selenium Grid et les différents Selenium RC dans notre build.xml :


    

Dans le hub.classpath on référence le chemin vers selenium-grid-hub-standalone-1.0.jar, et on peut également référencer un fichier grid_configuration.yml qui va surcharger celui livré dans la distribution et dans lequel on peut redéfinir le port utilisé par le hub (par défaut 4444) ainsi que les différentes configurations cibles des RC (browser et OS).

Voici un exemple de configuration pour les selenium RC :


    
      
      
      
      
      
      
      
         
    

Pour chaque serveur RC que l’on veut instancier, on peut créer une configuration similaire en changeant le port d’écoute ({{-port}}) et l’environnement cible ({{-env}} : ici *chrome correspond à Firefox).

Il faut maintenant s’assurer que nos tests Selenium envoient leurs requêtes au Hub. On va donc modifier notre classe précédente BaseSeleniumTestCase (qui s’occupe d’instancier Selenium et ouvrir le navigateur) et changer le port (dans l’idéal on ne modifie pas le code source des tests, ce paramétrage est externalisé dans un fichier de configuration) :

public class BaseSeleniumTestCase {
   
    // créé un nouvelle instance de Selenium et ouvre un browser
    protected void setUp ()
        throws Exception {
        super.setUp();
        selenium =
            new DefaultSelenium(
                "localhost",
                "4444",
                "*chrome",
                "http://localhost:8080/myapp/");
    }
...
}

Maintenant tout est prêt pour lancer les tests. On lance d’abord le hub avec ant launch-hub, puis les différents serveurs RC (en local ou sur d’autres machines) avec ant launch-remote-control1 ({{launch-remote-control2…n}}). Chacun des serveurs RC s’enregistre auprès du hub :

Désormais, il vous suffit d’exécuter vos tests Selenium et laisser la magie opérer !

Performances

Si vous avez une base de tests unitaires assez conséquente, à la première utilisation de Selenium Grid, vous allez très rapidement comprendre pourquoi vous ne pourrez plus désormais vous en passer !

En effet, dès l’ajout d’une 2ème instance Selenium RC, vous pourrez constater que le temps d’exécution est déjà divisé par 2. Lors de mes tests, j’ai lancé jusqu’à 4 serveurs RC en local sur un portable « standard » sans problème et j’ai pu quasiment diviser le temps d’exécution par 4. L’ajout d’un nouveau serveur RC peut se faire à la volée sans problème, ce qui permet d’adapter les ressources allouées selon ses besoins.

Et pas de problème si vous avez besoin de plus « grosses » performances, les concepteurs de Selenium annoncent clairement la couleur sur leur site : 5 machines puissantes exécutant chacune plus de 10 instances de serveurs RC suffisent à diviser le temps d’exécution par 50 !

Conclusion

Comme on a pu le voir, au même titre que Selenium, Selenium Grid s’intègre très facilement dans une configuration d’intégration continue et son utilisation se résume à lancer le Selenium grid server et à configurer les instances Selenium RC server pour qu’elle se connecte à celui-ci. L’ajout d’un nouveau serveur RC (en local ou en remote) est tout aussi simple et permet de scaler rapidement son environnement selon les besoins. La parallélisation des tests est un pré-requis, mais l’utilisation de ParallelJunit ou de TestNG permet de régler rapidement et facilement le problème.

10 réflexions au sujet de « Optimisez les performances de vos tests fonctionnels avec Selenium Grid »

  1. Publié par didier, Il y a 9 années

    Merci Alex, tres clair.

    on pourrait a l’extreme s’en servir pour lancer à peu de frais des tests de charge à partir de la base de tests fonctionnels Selenium?

  2. Publié par pascal, Il y a 9 années

    Je ne pense pas, comme les tests se déroulent dans un navigateur, si tu veux simuler 1000 connexions simultanées, Selenium lancerait donc 1000 navigateurs. Un peu lourd :D

  3. Publié par emmanuel, Il y a 9 années

    Merci pour cet article très instructif.
    Quelques questions toutefois :
    si vous deviez par exemple lancer toutes les nuits une batterie de tests depuis une machine dediée aux tests, comment feriez vous ? La machine en question serait la seule allumée et n’a peut être pas de navigateur. Autre question : est ce que c’est la machine utilisée dans -hubURL doit être capable de lancer un navigateur ?
    Et enfin est ce que cela un sens pour vous un projet avec 7 développeurs de la couche web qui ne ferait des tests que toutes les nuits et pas plus régulièrement par exemple à chaque commit ?

  4. Publié par Guillaume Carre, Il y a 9 années

    Bonjour Emmanuel,

    si la machine utilisée comme hub n’est pas aussi utilisée pour exécuter des tests, elle n’aura pas besoin de navigateur. En revanche les machines qui vont les exécuter ont bien entendu besoin d’un navigateur, et doivent rester allumées.
    Vous pouvez pour déclencher la batterie de tests utiliser un serveur d’intégration continue, par exemple Hudson, qui dispose d’un plugin VNC qui permettra d’observer à distance le comportement du build et du navigateur, pratique durant la phase de mise en place (c’est le plugin de choix pour dérouler les tests Selenium avec firefox sur un serveur Linux).

    En ce qui concerne la fréquence des builds, tout dépend de leur durée, et de la fréquence des commits. Il y a en effet peu de chances que le build qui exécute les tests Selenium soit très rapide (moins de 10 minutes), donc il sera difficile de l’exécuter à chaque commit. En revanche, l’utilisation de Selenium Grid permettra d’exécuter le build plus fréquemment qu’une fois par jour.

  5. Publié par Sylvain M., Il y a 9 années

    J’ai testé récemment selenium sur mon poste de travail, seul soucis je n’avais pas de serveur à disposition pour effectuer des tests en condition réelle. De ce que j’ai plus constater, selenium a besoin de lancer firefox, il a « normalement » (à vérifier sur un serveur sans serveur x) besoin d’un serveur x pour se lancer. Une solution est d’utiliser Xvfb qui est un simulateur de server x sous linux. Firefox est alors executé sur un autre Display que celui utilisé par les écrans (Firefox est executé en tâche de fond)
    Il y a même un plugin maven qui permet de lancer une instance de Xvfb

  6. Publié par Guillaume Carre, Il y a 9 années

    Oui, Xvfb est une solution. Comme je le disais plus haut un serveur VNC en est une autre, il ouvre aussi un display, et surtout on peut s’y connecter à distance, ce qui est appréciable.

  7. Publié par valerie, Il y a 8 années

    Merci, article très intéressant.
    Je suis en train de mettre en place les tests Selenium de mon projet sur un serveur d’intégration continue avec Hudson et de son plugin VNC … et bien, mon build est en échec, car Selenium n’arrive pas à lancer Firefox.
    Est ce que vous avez une explication? un conseil? Merci…

  8. Publié par Steph, Il y a 7 années

    Bonjour à tous !

    J’arrive un peu après la bataille mais je voulais juste savoir s’il y a moyen d’implémenter des batteries de tests parallèles en PHP en PHPUnit ?
    Parce que l’article ne traite que de la librairie ParallelJUnit pour Java mais y a-t-il quelque chose de similaire en PHP ? Je n’ai rien trouvé à ce sujet…

    Merci beaucoup :)

  9. Publié par Youssef, Il y a 5 années

    Je veux faire des tests automatiques de non régression pour une application web j’ai choisit de générer le test avec selenium RC sur Eclipse :
    voila le code d’un test simple :

    import com.thoughtworks.selenium.DefaultSelenium;
    import com.thoughtworks.selenium.Selenium;

    public class PremierTest {

    private static Selenium selenium;
    public static void main(String[] args)
    {
    selenium = new DefaultSelenium(« localhost », 4444, « *firefox C:/Program Files/Mozilla Firefox/firefox.exe », « http://www.google.com »);
    selenium.start();
    selenium.open(« / »);
    selenium.windowMaximize();
    }
    }
    le problème que lors du lancement d’execution une erreur apparait :
    Exception in thread « main » java.lang.RuntimeException: Could not start Selenium session: Failed to start new browser session: Error while launching browser
    at com.thoughtworks.selenium.DefaultSelenium.start(DefaultSelenium.java:109)
    at PremierTest.main(PremierTest.java:13)
    Caused by: com.thoughtworks.selenium.SeleniumException: Failed to start new browser session: Error while launching browser
    at com.thoughtworks.selenium.HttpCommandProcessor.throwAssertionFailureExceptionOrError(HttpCommandProcessor.java:112)
    at com.thoughtworks.selenium.HttpCommandProcessor.doCommand(HttpCommandProcessor.java:106)
    at com.thoughtworks.selenium.HttpCommandProcessor.getString(HttpCommandProcessor.java:275)
    at com.thoughtworks.selenium.HttpCommandProcessor.start(HttpCommandProcessor.java:237)
    at com.thoughtworks.selenium.DefaultSelenium.start(DefaultSelenium.java:100)
    … 1 more

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *