1. Introduction
Tester l’IHM (Interface Homme Machine) d’une application après l’ajout d’une nouvelle fonctionnalité ou la correction d’une anomalie peux prendre beaucoup de temps car il faut de nouveau tester l’affichage et les interactions possibles sur/entre chaque page :
- Affichage correcte des pages
- Fonctionnement de tous les liens présents sur chaque page
- Exécution correcte des scripts JavaScript
- Cas passants et non passants sur les formulaires (champs obligatoires, règles de gestion….)
- Etc…
Afin de pouvoir tester l’IHM de vos applications web de façon automatique, plusieurs solutions existent, dont la plus connue est Selenium. Cependant, cette solution n’est pas simple à mettre en œuvre, trop verbeuse et difficilement maintenable.
FluentLenium, développé par Mathilde Lemée permet de palier à ces soucis de complexité tout en garantissant la maintenabilité de votre code en proposant une API simplifiant l’écriture de ces tests Selenium.
Nous allons découvrir au travers de cet article comment l’utiliser dans vos projets.
2. Méthodes proposées par FluentSelenium
2.1 Les sélecteurs
FluentLenium se base sur les sélecteurs CSS1, CSS2 et CSS3 associés à la méthode « find() », par exemple pour récupérer la liste des éléments :
- Avec l’id « footer » : find(« #footer »)
- Ayant la classe « foo » : find(« .foo ») ;
- Tous les champs de type « input » : find(« input »)
2.2 Les filtres
Vous pouvez utiliser des filtres pour affiner votre recherche, exemple :
1 2 3 4 5 6 7 |
find(".bar", withName("foo")) find(".bar", withClass("foo")) find(".bar", withId("idOne")) find(".bar", withText("This field is mandatory.")) |
A noter que vous pouvez vous passer du sélecteur CSS et écrire directement :
1 |
find(withText("This field is mandatory.")) |
Il est également possible d’enchaîner les filtres pour ajouter des critères de restriction :
1 |
find((".bar", withText("This field is mandatory."), withName("foo")), withId("idOne"))) |
Il existe d’autres filtres sur les chaînes de caractère :
1 2 3 4 5 6 7 |
contains : contenant containsWord : contenant le mot notContains : qui ne contient pas la chaîne de caractère startsWith : commençant par notStartsWith : ne commençant pas par endsWith : terminant par notEndsWith : ne terminant pas par |
Comme dans l’exemple précédent vous pouvez ou non utiliser un sélecteur CSS avec ces filtres.
Enfin, si vous êtes plus familier avec la syntaxe Jquery, vous pouvez remplacer la méthode « find() » par « $ » :
1 |
goTo("http://mywebpage/");$("#firstName").text("toto");$("#create-button").click();assertThat(title()).isEqualTo("Hello toto"); |
2.3 Accéder aux propriétés des éléments sélectionnés
Exemples pour un seul élément :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
findFirst(myCssSelector).getName() : nom de l’élément findFirst(myCssSelector).getId() : id de l’élément findFirst(myCssSelector).getValue() : valeur de l’élément findFirst(myCssSelector).getTagName() : tag de l’élément findFirst(myCssSelector).getText() : texte de l’élément findFirst(myCssSelector).isDisplayed() : l’élément est-il visible findFirst(myCssSelector).isEnabled() : l’élément est-il activé findFirst(myCssSelector).isSelected() : l’élément est-il sélectionné findFirst(myCssSelector).html() : Récupérer le code Html de l’élément |
Exemples pour une liste d’éléments :
1 |
find(myCssSelector).getNames()find(myCssSelector).getIds()find(myCssSelector).getValues()find(myCssSelector).getAttributes("myCustomAttribute")find(myCssSelector).getTexts() |
2.4 Les formulaires
Remplir le formulaire
Pour remplir votre formulaire, FluentLenium fourni la méthode « fill() » :
Exemple pour remplir tous les champs avec la valeur « foo » :
1 |
fill("input").with("foo") oufind("input").text("foo") |
Vous pouvez également utiliser les sélecteurs CSS ou les filtres vus précédemment :
1 |
fill("input:not([type='checkbox'])").with("tomato")oufill("input", with("type", notContains("checkbox"))).with("tomato") |
Actions
Pour simuler un clic sur tous les boutons ayant pour id « myButton » :
1 |
click("#myButton") |
Pour effacer tous les champs ayant pour id « myField » :
1 |
clear("#myField ") |
Pour soumettre votre formulaire :
1 |
submit("#form") |
3. Écriture des tests
3.1 Utilisation du pattern page objet (Page Object Pattern)
Le pattern page objet permet de modéliser l’interface utilisateur (la page web) qui va ensuite être utilisée pour écrire vos tests unitaires.
Les pages vont être vues comme des services que l’on va injecter dans nos différents tests ce qui permet d’avoir un code beaucoup maintenable, réutilisable.
Pour construire un objet de type « page » avec FluentLenium vous devez étendre la classe « org.fluentlenium.core.FluentPage » et définir l’url de la page à tester en surchargeant la méthode « getUrl() ». Vous pourrez ensuite utiliser la méthode goTo(myPage) dans le code de votre test pour accéder à cette page.
Il peut être nécessaire de vérifier si la page sur laquelle vous êtes est vraiment la bonne, pour cela vous devez surcharger la méthode « isAt() » et y renseigner toutes les assertions nécessaires. Par exemple, si le titre de la page suffit pour l’identifier :
1 2 3 4 |
@Override public void isAt() { assertThat(title()).contains("Selenium"); } |
Enfin, renseigner dans ces pages toutes les méthodes susceptibles d’être utilisées, il sera ensuite beaucoup plus facile de les utiliser au sein de vos différents tests.
Exemple de page :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class LoginPage extends FluentPage { public String getUrl() { return "myCustomUrl"; } public void isAt() { assertThat(title()).isEqualTo("MyTitle"); } public void fillAndSubmitForm(String... paramsOrdered) { fill("input").with(paramsOrdered); click("#create-button"); } } |
Exemple de test associé :
1 2 3 4 5 |
public void checkLoginFailed() { goTo(loginPage); loginPage.fillAndSubmitLoginForm("login","wrongPass"); assertThat(find(".error")).hasSize(1); assertThat(loginPage).isAt(); } |
3.2 L’annotation @Page
Vous pouvez facilement injecter une ou plusieurs pages dans votre test unitaire en utilisant l’annotation « @Page », par exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class AnnotationInitialization extends FluentTest { public WebDriver webDriver = new HtmlUnitDriver(); @Page public LoginPage page; @Test public void test_no_exception() { goTo(page); //put your assertions here } @Override public WebDriver getDefaultDriver() { return webDriver; } } |
A noter que vous pouvez également injecter une page dans une autre page en utilisant cette même annotation.
3.3 Les WebElements
Les WebElements comme leur nom l’indique correspondent à tous les types d’élément HTML (bouton, champs, formulaire, etc…)
Au sein d’une page tous les objets de type FluentWebElement sont par défaut automatiquement recherchés par leur « id » ou leur attribut « name ».
Ainsi, si vous déclarer un bouton « myButton » de type FluentWebElement au sein de votre page, FluentSelenium va automatiquement rechercher au sein de votre interface utilisateur un élément ayant pour id ou pour nom « myButton » :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class LoginPage extends FluentPage { FluentWebElement createButton; public String getUrl() { return "myCustomUrl"; } public void isAt() { assertThat(title()).isEqualTo("MyTitle"); } public void fillAndSubmitForm(String... paramsOrdered) { fill("input").with(paramsOrdered); createButton.click(); } } |
Mais si l’id ou le nom de vos éléments HTML ne correspondent pas à la convention de nommage de Java, vous pouvez utiliser l’annotation « @FindBy() » afin de récupérer l’élément souhaité par sa classe CSS, son nom etc… :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class LoginPage extends FluentPage { @FindBy(css = "button.create-button") FluentWebElement createButton; public String getUrl() { return "myCustomUrl"; } public void isAt() { assertThat(title()).isEqualTo("MyTitle"); } public void fillAndSubmitForm(String... paramsOrdered) { fill("input").with(paramsOrdered); createButton.click(); } } |
Vous pouvez également créer vos propres WebElement avec leurs propres méthodes, il suffit pour cela que le constructeur de votre élément prenne en paramètre un objet de type WebElement.
Par exemple, si je souhaite créer un objet de type « MyButton » :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class LoginPage extends FluentPage { MyButton createButton; public void fillAndSubmitForm(String... paramsOrdered) { fill("input").with(paramsOrdered); createButton.clickTwice(); } public static class MyButton { WebElement webElement; public MyButton(WebElement webElement) { this.webElement = webElement; } public void clickTwice() { webElement.click(); webElement.click(); } } } |
Enfin, si vous devez attendre avant qu’un élément soit disponible, suite à un appel Ajax par exemple, vous pouvez utiliser l’annotation « @AjaxElement »:
1 2 3 4 |
public class LoginPage extends FluentPage { @AjaxElement FluentWebElement myAjaxElement; } |
Il est possible de spécifier un timeout avant que la page ne propage une erreur si l’élément n’est pas trouvé via la propriété « timeountOnSeconds » (par défaut le timeout est d’une seconde) :
1 2 3 4 |
public class LoginPage extends FluentPage { @AjaxElement(timeoutOnSeconds=3) FluentWebElement myAjaxElement; } |
3.4 Appels Ajax
FluentLenium propose une API permettant de gérer efficacement les appels Ajax.
Ainsi, si vous souhaitez attendre au maximum 5 secondes avant que le nombre d’éléments (3 dans cet exemple) correspondant à votre critère de recherche soit affiché :
1 |
await().atMost(5, TimeUnit.SECONDS).until(".small").hasSize(3); |
A noter que par défaut, le temps d’attente avant de propager une erreur est de 500 ms.
Nous venons de voir un exemple avec une vérification sur la taille mais il existe d’autres méthodes :
1 2 3 4 5 6 7 8 |
hasText("myTextValue") isPresent() isNotPresent() hasId("myId") hasName("myName") containsText("myName") areDisplayed() areEnabled() |
Vous pouvez ajouter des restrictions en utilisant les filtres et les matchers vus précédemment, exemple :
1 2 |
await().atMost(5,TimeUnit.SECONDS).until(".small").withText().startsWith("start").isPresent(); await().atMost(5,TimeUnit.SECONDS).until(".small").with("myAttribute").startsWith("myValue").isPresent(); |
De la même façon que pour attendre suite à un appel Ajax, vous pouvez placer un timer pour récupérer vos éléments toutes les X (secondes, minutes…), en utilisant la méthode « pollingEvery() »:
1 |
await().pollingEvery(5,TimeUnit.SECONDS).until(".small").with("myAttribute") |
3.5 Exécuter du JavaScript
Si vous avez besoin d’exécuter un script JavaScript, vous devez utiliser la méthode « executeJavascript » avec le nom de la méthode à exécuter en paramètre :
1 |
executeScript("change();"); |
Vous pouvez aussi passer des arguments à votre méthode JavaScript et de manière asynchrone avant de récupérer le résultat :
1 |
executeScript("change();", 12L).getStringResult(); |
Il est aussi possible d’écrire directement du code JavaScript au sein de cette méthode :
1 |
executeScript("alert(‘foo‘) ; "); |
3.6 Prendre une capture d’écran
Vous pouvez prendre une capture d’écran de votre navigateur pendant l’exécution des tests en utilisant la méthode « takeScreenShot() » et passer en paramètre le chemin et le nom du fichier à sauvegarder :
1 |
takeScreenShot(pathAndfileName); |
4. Cas concret
Nous allons voir étape par étape comment utiliser FluentLenium au sein d’un projet Liferay.
Nous utiliserons Eclipse comme IDE.
4.1 Créer un nouveau module de test
Afin de séparer mes tests d’IHM, je vais créer un nouveau module pour mon projet que je nomme : « test-ihm » :
Dans File -> New -> Other -> Maven module
4.2 Ajouter la dépendance FluentLenium
Pour ajouter la dépendance FluentLenium à votre projet, ouvrez le fichier « pom.xml » du module précédemment créé et copier le code suivant :
1 2 3 4 5 6 |
<dependency> <groupId>org.fluentlenium</groupId> <artifactId>fluentlenium-assertj</artifactId> <version>0.10.3</version> <scope>test</scope> </dependency> |
Nous utiliserons AssertJ pour l’écriture de nos tests mais si vous souhaitez utiliser Junit vous pouvez remplacer le code précédent par :
1 2 3 4 5 6 |
<dependency> <groupId>org.fluentlenium</groupId> <artifactId>fluentlenium-core</artifactId> <version>0.10.3</version> <scope>test</scope> </dependency> |
En effet, par défaut, le core de FluentLenium propose un adaptateur pour écrire des tests JUnit mais il existe aussi des adaptateurs pour AssertJ et TestNG
1 2 3 4 5 6 |
<dependency> <groupId>org.fluentlenium</groupId> <artifactId>fluentlenium-assertj/testng</artifactId> <version>0.10.3</version> <scope>test</scope> </dependency> |
Placez-vous dans le dossier correspondant à votre module puis lancer un build maven en tapant la commande :
1 |
mvn clean install eclipse:eclipse |
4.3 Tests de notre page de login
Nous allons tester la page de login du projet :
Arborescence du projet :
- Créer le package « fr.ihm.page » dans le dossier src/test/java
- Créer la classe « AbstractPage.java » et faites la étendre la classe « FluentPage »
- Créer ensuite la classe « LoginPage.java » et faites la étendre la classe « AbstractPage.java»
- Notre page de login contient un formulaire avec 2 champs (login et password) et un bouton pour soumettre le formulaire, nous allons les ajouter dans notre classe « LoginPage.java » :
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 |
@Component public class LoginPage extends AbstractPage { // Titre de la page private static final String PAGE_TITLE = "Accueil"; // Bouton de soumission du formulaire @FindBy(className = "btn_signin") private FluentWebElement submitButton; /** * @return l'url courante de notre page */ @Override public String getUrl() { return ""; } /** * Vérifie qu'on est bien sur notre page de login */ @Override public void isAt() { assertThat(title()).isEqualTo(PAGE_TITLE); } /** * Méthode permettant de remplir et soumettre le formulaire de login * * @param paramsOrdered : tableau de String contenant les paramètres à passer au formulaire */ public void fillAndSubmitForm(String... paramsOrdered) { fill(getLoginInput()).with(paramsOrdered[0]); fill(getPasswordInput()).with(paramsOrdered[1]); submitButton.click(); } /** * @return l'input de login */ public FluentWebElement getLoginInput() { return $("input[name*='login']").get(0); } /** * @return l'input du mot de passe */ public FluentWebElement getPasswordInput() { return $("input[name*='password']").get(0); } } |
4.4 Création des tests associés
Nous allons tester la page précédemment créée :
- Créer le package « fr.ihm.integration » dans le dossier src/test/java
- Créer la classe « AbstractPageTest.java » et faites la étendre la classe « FluentTest »
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 |
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:applicationContext-test.xml") public class AbstractPageTest extends FluentTest { /** * Url de l'application */ @Value("${application.url}") protected String applicationUrl; /** * Login administrateur */ @Value("${admin.login}") protected String adminLogin; /** * Password administrateur */ @Value("${admin.password}") protected String adminPassword; // Déclaration du driver à utiliser public WebDriver webDriver = new HtmlUnitDriver(); @Test public void testAbstractTest() { Assert.assertTrue(true); } /** * Surcharge du driver par défaut */ @Override public WebDriver getDefaultDriver() { return webDriver; } /** * Surcharge de l'url par défaut */ @Override public String getDefaultBaseUrl() { return applicationUrl; } } |
- Créer la classe « LoginPageTest.java » et faites la étendre la classe « AbstractPageTest»
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 |
package fr.ihm.integration; import static org.fluentlenium.assertj.FluentLeniumAssertions.assertThat; import org.fluentlenium.adapter.FluentTest; import org.fluentlenium.core.annotation.Page; import org.junit.Test; import org.openqa.selenium.WebDriver; import org.openqa.selenium.htmlunit.HtmlUnitDriver; import fr.ihm.integration.LoginPage; public class LoginPageTest extends FluentTest { /** * Injection de la page de login à tester */ @Page private LoginPage loginPage; /** * Vérification que l'identification en tant qu'administrateur fonctionne */ @Test(expected = NoSuchElementException.class) public void checkLoginSuccess() { loginPage.goTo(loginPage); loginPage.fillAndSubmitForm(adminLogin, adminPassword); assertThat($("div.portlet-msg-error").first()).isNull(); loginPage.isAt(); } /** * Vérification que l'authentification avec un mauvais couple identifiant/mot de passe échoue */ @Test public void checkLoginFailed() { goTo(loginPage); loginPage.fillAndSubmitForm("login", "wrongPass"); assertThat($("div.portlet-msg-error").get(0)).isNotNull(); assertThat(loginPage).isAt(); } |
4.5 Lancement des tests
- Démarrer le serveur contenant la webapp à tester
- Une fois le démarrage terminé, faites un clic droit sur le dossier src/test/java à Run As à Junit Test
- Tous les tests vont s’exécuter, vous devriez avoir le résultat suivant à la fin de leur exécution :
5. Automatisation des tests
Nous allons voir comment automatiser le lancement de ces tests durant le cycle de vie du build Maven sans avoir à démarrer manuellement notre serveur Liferay :
- Dans le fichier « pom.xml » nous allons déclarer 2 profils, un pour les tests en local et l’autre pour les tests sur la plateforme d’intégration :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<profiles> <profile> <id><u>dev</u></id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <profile.name>dev</profile.name> </properties> </profile> <profile> <id>test</id> <properties> <profile.name>test</profile.name> </properties> </profile> </profiles> |
- On configure ensuite le build pour lancer nos tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!-- Location <u>des</u> <u>ressources</u> à <u>utiliser</u> <u>en</u> <u>fonction</u> <u>du</u> <u>profil</u> --> <resources> <resource> <directory><u>src</u>/test/resources</directory> <includes> <include>*.properties</include> </includes> </resource> <resource> <directory><u>src</u>/test/resources/${profile.name}</directory> <includes> <include>*.properties</include> </includes> </resource> </resources> |
- Nous allons utiliser plusieurs plugins pour l’exécution de nos tests, le premier, nommé « cargo-maven2-plugin » va nous permettre de démarrer/stopper notre serveur Liferay :
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 |
<plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-<u>plugin</u></artifactId> <version>1.4.14</version> <executions> <execution> <id>start-<u>liferay</u></id> <phase><u>pre</u>-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <id>stop-<u>liferay</u></id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> <configuration> <skip>false</skip> <wait>false</wait> <container> <containerId>tomcat7x</containerId> <home>${liferay.home}/tomcat-7.0.40</home> <timeout>300000</timeout> <type>installed</type> <systemProperties> <file.encoding>UTF8</file.encoding> <external-properties>${liferay.home}/portal-ext.properties</external-properties> </systemProperties> </container> <configuration> <home>${liferay.home}/tomcat-7.0.40</home> <type>existing</type> <properties> <cargo.jvmargs>-Xmx1024m -XX:MaxPermSize=512m</cargo.jvmargs> <cargo.servlet.port>8080</cargo.servlet.port> <cargo.logging>high</cargo.logging> </properties> </configuration> </configuration> </plugin> |
- Le plugin « maven-antrun-plugin » va s’exécuter pendant la phase « clean » du build et va nous permettre de supprimer les fichiers temporaires utilisés par Liferay afin d’exécuter nos tests dans un environnement sain :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<plugin> <artifactId><u>maven</u>-<u>antrun</u>-<u>plugin</u></artifactId> <executions> <execution> <id>clean-<u>liferay</u></id> <phase>clean</phase> <configuration> <tasks> <echo>Cleaning Liferay..</echo> <delete dir<em>="${liferay.home}/tomcat-7.0.40/work"</em> quiet=<em>"true"</em> /> <delete dir=<em>"${liferay.home}/tomcat-7.0.40/temp"</em> quiet=<em>"true"</em> /> </tasks> </configuration> <goals> <goal>run</goal> </goals> </execution> </executions> </plugin> |
- Le plugin failsafe permet de lancer les tests d’intégration pendant le build maven
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId><u>maven</u>-<u>failsafe</u>-<u>plugin</u></artifactId> <version>2.9</version> <executions> <execution> <id>fluentLenium-test</id> <phase>integration-test</phase> <goals> <goal>integration-test</goal> </goals> <configuration> <includes> <include>**/**Test.java</include> </includes> <reportsDirectory>${project.build.directory}/failsafe-reports/fluentLenium</reportsDirectory> </configuration> </execution> </executions> </plugin> |
- Le plugin surefire utilisé par le plugin cargo pour lancer les tests
1 2 3 4 5 6 7 8 |
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId><u>maven</u>-<u>surefire</u>-<u>plugin</u></artifactId> <version>2.18.1</version> <configuration> <skip>true</skip> </configuration> </plugin> |
En local, il suffit de taper la commande « mvn clean install » pour builder notre projet et exécuter les tests automatiquement.
Si on souhaite utiliser une solution d’intégration continue comme Jenkins nous devrons préciser le profil de test et taper la commande « mvn clean install –P test ».
6. Conclusion
Nous sommes souvent rebuté à l’idée d’écrire des tests d’intégration pour l’IHM de nos projets car beaucoup d’entre nous savent que l’utilisation de Selenium est complexe et peu maintenable.
Au travers cette présentation, nous avons pu voir qu’il est aisé d’écrire ce type de tests en utilisant FluentLenium.
L’utilisation du pattern page objet rend le code beaucoup plus maintenable et l’API proposée par Mathilde Lemée facilite l’écriture mais aussi la lisibilité de nos tests.
Nous n’avons donc plus aucune excuse pour ne pas l’utiliser au sein de nos projets !
7. Annexes
GitHub | https://github.com/FluentLenium/FluentLenium |
Tutoriel pour tester une webapp à l’aide de FluentLenium, en 5 minutes | http://thierry-leriche-dessirier.developpez.com/tutoriels/javaweb/tester-webapp-fluentlenium-5min/ |
Présentation par Mathilde Lemée | http://fr.slideshare.net/MathildeLemee/fluentlenium |