Accueil > Non classé > Des tests fonctionnels pour des projets mavenisés : un exemple concret avec Liferay

Des tests fonctionnels pour des projets mavenisés : un exemple concret avec Liferay

Aujourd’hui je vous parlerai de la mise en place de tests fonctionnels dans le cadre de l’intégration continue, et l’illustrerai par un exemple concret de projet de type plugin se déployant dans un portail comme Liferay. En effet, plusieurs serveurs d’intégration (Hudson/Jenkins, Bamboo) permettent d’effectuer une série d’opérations sur le code après chaque commit sur le repository ou à heure définie, pour vérifier son intégrité et son bon comportement. Dans le monde de l’entreprise, une telle plateforme est souvent mise en place sur une machine dédiée, pour vérifier la compilation, lancer les tests unitaires, et tenter le déploiement de l’application. A la charge des développeurs, ou d’une équipe dédiée de testeurs, de voir si les nouveaux développements n’ont pas provoqué de régressions fonctionnelles sur le reste de l’application web…

 

Et les tests fonctionnels ?

 

Justement, ils permettent d’effectuer de manière automatique, une série de clics et de saisie clavier pour effectuer un scénario fonctionnel. L’avantage, c’est qu’ils sont lancés automatiquement à chaque build et testent l’application de manière transverse via l’interface graphique. En effet, si les tests unitaires de deux classes isolées passent bien, le rendu de l’assemblage de ces deux briques peut ne pas être celui attendu.

 

Mais comment ça marche ?

Selenium, of course !

 

Des bibliothèques existent pour commander le navigateur, comme Selenium : il suffit de programmer son scénario avec Selenium API (existe en module Firefox ici : http://seleniumhq.org/download/), et d’exporter le test dans son langage de prédilection : nous illustrerons par la suite un cas de test en Java.

Grâce à la bibliothèque Selenium, il suffit d’étendre SeleneseTestCase ou d’utiliser un objet de type Selenium. L’inconvénient de SeleneseTestCase est d’ouvrir et fermer le navigateur avant et après chaque test alors que ce n’est pas forcément utile. Il n’y a pas de distinction entre les annotations @Before et @BeforeClass.

Pour pallier à ce problème, on peut coder un test Selenium de cette manière :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.excilys.monprojet.monportlet.selenium;

import static org.junit.Assert.assertTrue;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

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

public class NavigationIT {

    private static final String TIMEOUT_PAGE = "30000";
    private static Selenium selenium;

    @BeforeClass
    public static void setUp() throws Exception {
        String url = "http://localhost:8080/liferay/";

        selenium = new DefaultSelenium("localhost", 4444, "*firefox", url);
        selenium.start();
    }

    @AfterClass
    public static void tearDown() {
        selenium.close();
        selenium.stop();
    }

    @Before
    public void beforeLogin() {
        selenium.open("");
        selenium.waitForPageToLoad(TIMEOUT_PAGE);
    }

    @After
    public void afterLogout() {
    }

    @Test
    public void testPorletAffiche() {
        assertTrue(selenium.isTextPresent("Portlet Agenda"));
    }

}

Nous venons donc de coder un test fonctionnel qui à travers le navigateur Firefox, ouvre une nouvelle page, accède à l’URL http://localhost:8080/liferay/, attend que la page soit chargée, se logge, et vérifie que le texte “Portlet Agenda” est bien présent.

 

Et une fois le scénario écrit, comment automatiser les tests ?

Pom Maven, Surefire, Ant, et autres douceurs…

 

Avec Maven, on configure un nouveau profil spécifique aux tests fonctionnels. En effet, on s’arrange pour ne pas effectuer ces tests un peu longs dans le profil par défaut du build Maven. Cela permet de gagner du temps dans le développement et de ne pas être dérangé par des fenêtres Firefox intempestives…

Dans le pom du projet, on ajoute donc le profil “integration-test” :


1
2
3
4
5
6
7
8
9
10
<profiles>
    <profile>
        <id>integration-test</id>
        <activation>
            <activeByDefault>false</activeByDefault>
        </activation>
        <build>
        </build>
    </profile>
</profiles>

Le build prend en compte cette configuration quand on lance le build Maven via la commande :


1
mvn clean install -Pintegration-test

 

Plugin maven-surefire

 

On utilise Surefire pour lancer les tests situés dans un certain package. Si ce plugin est utilisé pour exécuter les tests unitaires JUnit, rien n’empêche de l’appliquer aux tests Selenium qui seront traités de la même manière.

Entre les balises <build></build>, on ajoute :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${surefire.plugin.version}</version>
        <executions>
            <!-- Tests d'integration -->
            <execution>
                <id>surefire-integration-tests</id>
                <phase>integration-test</phase>
                <goals>
                    <goal>test</goal>
                </goals>
                <configuration>
                    <skip>false</skip>
                    <includes>
                        <include>**/selenium/**IT.java</include>
                    </includes>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

On va donc, pendant la phase integration-test, lancer les tests situés dans src/test/java/…/selenium/ dont le nom de classe se termine par IT.

 

Plugin selenium-maven

 

Avant de pouvoir lancer les tests d’intégration fonctionnels, il faut démarrer Selenium RC de manière automatique, et ensuite l’arrêter. En effet, Selenium “Remote Control” agit comme un proxy HTTP pour exécuter les requêtes navigateurs.

Entre les balises <plugins></plugins>, on ajoute :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>selenium-maven-plugin</artifactId>
    <version>${selenium.plugin.version}</version>
    <executions>
        <!-- Demarrage du serveur SeleniumRC -->
        <execution>
            <id>start</id>
            <phase>pre-integration-test</phase>
            <goals>
                <goal>start-server</goal>
            </goals>
            <configuration>
                <background>true</background>
            </configuration>
        </execution>
        <!-- Arret du serveur SeleniumRC -->
        <execution>
            <id>stop</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>stop-server</goal>
            </goals>
        </execution>
    </executions>
</plugin>

 

Plugin maven-antrun

 

Pour effectuer les tests fonctionnels, on a souvent besoin de déployer ses applications web au sein d’un conteneur de servlets de type Apache Tomcat, dont on doit pouvoir gérer le démarrage et l’extinction. C’est le cas pour une application se basant sur le portail Liferay, pour laquelle il faut aussi gérer le redéploiement de tous les plugins développés.

Pour cela, on crée un script de tâches Ant. Le gros avantage est de maîtriser l’ordre des commandes exécutées, alors que ce n’est pas forcément le cas lorsqu’on utilise des plugins dédiés différents dans une même phase.

Voici un exemple d’utilisation des tâches Ant dans un script pour Tomcat, qui démarre le serveur, attend le déploiement de la webapp Liferay, déploie une autre webapp qui est en fait un plugin Liferay, et arrête finalement le serveur. En phase de post-integration-test, on effectue les tâches contraires :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<properties>
    <antrun.plugin.version>1.7</antrun.plugin.version>
    <monprojet.monportlet.it.tomcat.dir>/opt/tomcat-liferay</monprojet.monportlet.it.tomcat.dir>
    <monprojet.monportlet.it.liferay.auto.deploy.dir>/opt/tomcat-liferay/liferay-deploy</monprojet.monportlet.it.liferay.auto.deploy.dir>
    <monprojet.monportlet.it.liferay.version>6.0.6</monprojet.monportlet.it.liferay.version>
    <monprojet.monportlet.it.url.liferay>http://localhost:8080/liferay</monprojet.monportlet.it.url.liferay>
</properties>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>${antrun.plugin.version}</version>
    <executions>
        <!-- Demarrage de Tomcat, deploiement du portail Liferay, deploiement des portlets -->
        <execution>
            <id>start-liferay</id>
            <phase>pre-integration-test</phase>
            <configuration>
                <target>
                    <echo>Starting Tomcat and Liferay server...</echo>
                    <echo>Tomcat directory : ${monprojet.monportlet.it.tomcat.dir}</echo>
                    <echo>Liferay auto-deploy directory : ${monprojet.monportlet.it.liferay.auto.deploy.dir}</echo>
                    <echo>Liferay version : ${monprojet.monportlet.it.liferay.version}</echo>
                    <exec dir="${monprojet.monportlet.it.tomcat.dir}/bin" executable="${monprojet.monportlet.it.tomcat.dir}/bin/startup.sh"
                        failonerror="true" />
                    <echo>Start signal send</echo>
                    <echo>Waiting for Liferay portal to be deployed...</echo>
                    <waitfor maxwait="5" maxwaitunit="minute" checkevery="1000">
                        <http url="${monprojet.monportlet.it.url.liferay}/" />
                    </waitfor>
                    <echo>Liferay deployed !</echo>
                    <echo>Copying ${artifactId}-${version}.war into Liferay auto-deploy directory...</echo>
                    <copy file="${basedir}/target/${artifactId}-${version}.war" tofile="${monprojet.monportlet.it.liferay.auto.deploy.dir}/${artifactId}.war" />
                    <echo>${artifactId}.war copied !</echo>
                    <echo>Waiting for ${artifactId} to be deployed...</echo>
                    <waitfor maxwait="5" maxwaitunit="minute" checkevery="5000">
                        <http url="${monprojet.monportlet.it.url}/${artifactId}/" />
                    </waitfor>
                    <echo>Portlet deployed !</echo>
                </target>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
        <!-- Arret de Tomcat -->
        <execution>
            <id>stop-liferay</id>
            <phase>post-integration-test</phase>
            <configuration>
                <target>
                    <echo>Shutting down Tomcat server...</echo>
                    <exec dir="${monprojet.monportlet.it.tomcat.dir}/bin" executable="${monprojet.monportlet.it.tomcat.dir}/bin/shutdown.sh"
                        failonerror="true" />
                    <echo>Stop signal send</echo>
                    <echo>Waiting 15 seconds for Tomcat to stop...</echo>
                    <waitfor maxwait="15" maxwaitunit="second" checkevery="15000">
                        <available file="inexistant.log" />
                    </waitfor>
                    <echo>Undeploying portlet ${artifactId}...</echo>
                    <delete includeemptydirs="true" verbose="false" failonerror="true">
                        <fileset dir="${monprojet.monportlet.it.tomcat.dir}/webapps/" includes="${artifactId}/**" />
                    </delete>
                    <echo>Portlet undeployed !</echo>
                </target>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
</plugin>

 

Et si je souhaite lancer mes tests fonctionnels en local et/ou sur la machine d’intégration ?

Profil Maven

 

Il suffit de paramétrer deux configurations différentes, et de rendre le projet indépendant de ces paramètres qui seront eux propres à chaque machine.

Pour cela, on configure le fichier settings.xml (dans C:\Users\Nom\.m2\, /home/user/.m2 ou /opt/maven/conf) chargé avant chaque build Maven. Après avoir repéré le profil par défaut, on ajoute nos propriétés : elles se nomment convenablement pour ne pas mélanger les configurations entre projets.

On doit obtenir quelque chose du type :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<settings>
    <mirrors>
        ...
    </mirrors>
    <profiles>
        <profile>
            <id>BLR</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <monprojet.monportlet.it.propriete1>valeur1</monprojet.monportlet.it.propriete1>
                <monprojet.monportlet.it.propriete2>valeur2</monprojet.monportlet.it.propriete2>
            </properties>
            <repositories>
                ...
            </repositories>
            <pluginRepositories>
                ...
            </pluginRepositories>
        </profile>
    </profiles>
    <servers>
        ...
    </servers>
</settings>

Dans notre cas on déplace le bloc des propriétés définissant l’environnement Liferay du pom vers settings.xml :


1
2
3
4
5
6
<properties>
    <monprojet.monportlet.it.tomcat.dir>/opt/tomcat-liferay</monprojet.monportlet.it.tomcat.dir>
    <monprojet.monportlet.it.liferay.auto.deploy.dir>/opt/tomcat-liferay/liferay-deploy</monprojet.monportlet.it.liferay.auto.deploy.dir>
    <monprojet.monportlet.it.liferay.version>6.0.6</monprojet.monportlet.it.liferay.version>
    <monprojet.monportlet.it.url.liferay>http://localhost:8080/liferay</monprojet.monportlet.it.url.liferay>
</properties>

Dans le pom, on peut alors faire appel aux variables qui sont disponibles pour tous les projets en utilisant la syntaxe :


1
${monprojet.monportlet.it.tomcat.dir}

 

Il reste l’URL en dur dans le test Selenium !

Filters et properties

 

Parfois, on a également besoin d’utiliser une propriété de settings.xml dans une classe de test Selenium : pour définir l’adresse où s’effectuent les tests Selenium par exemple :


1
2
selenium = new DefaultSelenium("localhost", 4444, "*firefox", url);
selenium.start();

Ici on veut que “url” prenne la valeur de la propriété qui est définie dans settings.xml et qui est prise en compte lorsque le pom du projet est parsé.

Pour cela, on crée un fichier it.properties dans src/test/resources, qui contient par exemple :


1
projet-url=${monprojet.monportlet.it.url}

(monprojet.monportlet.it.url est une propriété définie dans settings.xml)

On définit ensuite un filtre dans pom.xml, qui permet d’assigner les variables dans ce fichier lors des phases de tests. Entre les balises <build></build> du profil “integration-test”, on ajoute :


1
2
3
4
5
6
<testResources>
    <testResource>
        <directory>src/test/resources</directory>
        <filtering>true</filtering>
    </testResource>
</testResources>

Dans la classe de test, on peut alors écrire :


1
2
3
4
5
6
Properties props = new Properties();
props.load(ClassLoader.getSystemResource("it.properties").openStream());
String url = props.getProperty("projet-url") + "/";

selenium = new DefaultSelenium("localhost", 4444, "*firefox", url);
selenium.start();

 

Tout ça pour rien, ma machine d’intégration ne dispose pas d’environnement graphique…

Xvfb et jargon d’admin

 

Que nenni ! Il est possible de configuer un serveur xvfb pour rediriger la sortie graphique et effectuer vos tests dans un navigateur “invisible”.

Ce sera une étape obligatoire si, en lançant le build Maven avec le profil d’intégration, vous obtenez l’erreur frustrante :

java.lang.RuntimeException: Timed out waiting for profile to be created!
Failed to start new browser session: Error while launching browser on session null

 

Cette erreur s’accompagne d’un warning en jaune dans la console de Jenkins :

Waiting for Selenium Server...
[WARNING] OS appears to be Unix and no DISPLAY environment variable has been detected. Browser maybe unable to function correctly. Consider using the selenium:xvfb goal to enable headless operation.

 

Cela signifie que la machine sur laquelle vous tentez de lancer les tests fonctionnels ne dispose pas d’environnement graphique. On ne peut donc pas lancer la commande “firefox” pour ouvrir un navigateur.

Il faut demander à un administrateur d’effectuer les modifications suivantes sur la machine :

  • installer le package xvfb (un X11 server)
  • installer le package iceweasel (firefox pour une Debian)
  • configurer le display pour pouvoir exécuter des applications graphiques comme firefox : export DISPLAY=:99 (ou autre valeur), dans le contexte utilisé au lancement de Jenkins par exemple.
  • tester en lancant la commande Xvfb (il choisit le DISPLAY automatiquement), et lancer firefox en ligne de commande. Cela doit être fait avec le même utilisateur que celui qui lance Jenkins.

 
 
C’est fini ! Lancez autant de build maven en profil “integration-test” que vous souhaitez pour vous féliciter :)

Share
Categories: Non classé Tags: , ,
  1. Pascal
    11/01/2012 à 15:47 | #1

    C’est limpide, merci.

  2. 06/03/2015 à 16:48 | #2

    Une autre approche pour être sur d’éviter la faute de frappe entre le nom de la méthode et la clé de la propriété est celle d’android ou de play, la compilation de la configuration, poc est dispo là http://www.filmrally.com/

  1. Pas encore de trackbacks