Thucydides

L'objet de cet article, inspiré fortement par le guide utilisateur de Thucydides, et de mettre en place des tests de validation fonctionnelle sur une application Web à l'aide de Thucydides, donc, et de WebDriver/Selenium 2. Thucydides est une bibliothèques OpenSource destinée à faciliter ces tests de recette, en utilisant, soit JUnit, soit EasyB.

Vocabulaire

Avant tout, un peu de vocabulaire que nous allons rencontrer tout au long de notre petite expérimentation.

User Story (histoire d'utilisateur): à ne pas confondre avec un cas d'utilisation (Use case). Cf. http://www.qualitystreet.fr/2009/02... pour une explication comparative. Il s'agit d'une brève description d'un petit morceau de besoin fonctionnel qui a de la valeur pour l'utilisateur fonctionnel, une partie d'un Use Case (un scénario nominal ou alternatif) utilisé surtout pour l'estimation et la planification (par opposition à la spécification), qui sera implémenté et testé obligatoirement en une seule itération.

Feature: une fonctionnalité essentielle, un service fourni par le système, observable de l'extérieur, et qui remplit directement un besoin d'une partie-prenante (stakeholder).

Entre les deux, on va trouver les thèmes (regroupement de plusieurs Stories) facilitant leur priorisation et les Epopées (Epic), grosses Stories, trop grosses pour une seule itération en attente de décomposition.

Si on récapitule:

Feature > Theme > Epic > Story

Description de l'application

Maintenant, pour faire simple, nous allons partir d'un produit fini comme s'il s'agissait de notre application, en l’occurrence Wikipédia. En tant qu'utilisateur, deux Features nous intéressent:

  • la recherche
  • la contribution

Outillage

Installer Java et Maven

Oui, ça paraît évident, mais rappelons tout de même qu'il faut avoir un JDK Java et maven.

Pour le JDK:

  $ sudo apt-get install openjdk-6-jdk

Pour Maven, télécharger Apache Maven 3.0.4, le dézipper et

  • ajouter M2_HOME pointant vers le répertoire créé
  • ajouter M2 pointant vers le sous-répertoire bin de ce répertoire
  • ajouter M2 au PATH

Vérifier que tout est ok avec:

  $ mvn -v

J'ai également ajouté JAVA_HOME pour éviter un avertissement de Maven, même s'il avait déduit la valeur correcte. Mais je n'aime pas les warnings....

Configurer Maven

Les dépendances Thucydides sont disponibles dans le dépôt Maven OSS Sonatype (http://oss.sonatype.org/). Les livraisons peuvent être obtenues depuis le dépôt de release (https://oss.sonatype.org/content/repositories/releases/). Les instantanés sont disponibles depuis le dépôt Sonatype OSS Snapshot repository (https://oss.sonatype.org/content/repositories/snapshot/). Pour obtenir des dépendances de ces dépôts, vous aurez besoin d'ajouter une référence de dépôt soit à votre settings.xml, soit à votre dépôt Maven d'entreprise local (ex Nexus, Archiva ou Artifactory).

Pendant que vous éditerez votre fichier settings.xml, vous aurez également besoin d'ajouter une nouvelle entrée pluginGroup de telle sorte que vous puissiez lancer la génération de rapport Thucydides en ligne de commandes plus facilement:

Un fichier settings.xml minimaliste correspondant à cette configuration pourra ressembler à ceci:

  <?xml version="1.0" encoding="UTF-8"?>
  <settings>
     <pluginGroups>
         <pluginGroup>net.thucydides.maven.plugins</pluginGroup>
    </pluginGroups>
  
     <profiles>
         <profile>
             <id>personal</id>
             <activation>
                 <activeByDefault>true</activeByDefault>
             </activation>
             <repositories>
                  <repository>
                      <id>sonatype-oss-releases</id>
                      <url>https://oss.sonatype.org/content/repositories/releases</url>
                  </repository>
                  <repository>
                      <id>sonatype-oss-snapshots</id>
                      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
                  </repository>
              </repositories>
              <pluginRepositories>
                  <pluginRepository>
                      <id>sonatype-oss-releases</id>
                      <url>https://oss.sonatype.org/content/repositories/releases</url>
                  </pluginRepository>
                  <pluginRepository>
                      <id>sonatype-oss-snapshots</id>
                      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
                  </pluginRepository>
              </pluginRepositories>
          </profile>
      </profiles>
  </settings>

Créer un projet de test

Ensuite, vous avez besoin de créer un nouveau projet Maven pour vos tests web. Vous pouvez également intégrer les tests web Thucydides dans les tests d'intégration d'un projet existant en lançant votre application avec Jetty par exemple, mais je préfère garder les tests comme un projet séparé de telle sorte qu'ils puissent facilement être exécutés sur n'importe quel serveur.

Si vous n'êtes pas sûr de savoir créer un nouveau projet Maven depuis votre IDE, vous pouvez simplement le créer depuis la ligne de commande en utilisant la commande archetype:generate. Choisissez simplement les options par défaut et n'importe quel nom de groupe et de package qui vous convient,comme illustré ici:

  $ mvn archetype:generate 
  ...  
  Define value for property 'groupId': : fr.poum
  Define value for property 'artifactId': : thutoweb
  Define value for property 'version': 1.0-SNAPSHOT: 
  Define value for property 'package': fr.poum
  Confirm properties configuration:
  groupId: fr.poum
  artifactId: thutoweb
  version: 1.0-SNAPSHOT
  package: fr.poum
  Y: 
  INFO 
  INFO Using following parameters for creating project from Old (1.x) Archetype: maven-archetype-quickstart:1.1
  INFO 
  INFO Parameter: groupId, Value: com.wakaleo.webtests.wikipedia
  INFO Parameter: packageName, Value: com.wakaleo.webtests.wikipedia
  INFO Parameter: package, Value: com.wakaleo.webtests.wikipedia
  INFO Parameter: artifactId, Value: wikipediatests
  INFO Parameter: basedir, Value: /Users/johnsmart
  INFO Parameter: version, Value: 1.0-SNAPSHOT
  INFO ********************* End of debug info from resources from generated POM ***********************
  INFO project created from Old (1.x) Archetype in dir: /Users/johnsmart/wikipediatests
  INFO 
  INFO BUILD SUCCESSFUL
  INFO 
  INFO Total time: 2 minutes 11 seconds
  INFO Finished at: Tue Jun 28 10:20:12 EST 2011
  INFO Final Memory: 12M/81M
  INFO 

Ajouter les dépendances Thucydides au projet Maven

Maintenant, vous devez ajouter les dépendances Thucydides à votre projet. Dans le fichier pom.xml, mettez à jour les dépendances JUnit à au moins 4.8.2 et ajoutez une dépendance à hamcrest-all et thucydides-junit. Thucydides utilise SLF4J pour sa génération de log, aussi il vous faut aussi ajouter une implémentation de SLF4J (par exemple, slf4j-simple). Si vous n'utilisez pas Maven 3, assurez-vous de configurer le plugin compiler pour qu'il utilise Java 5. Enfin, ajoutez le plugin thucydides-maven-plugin qui fournit les services de génération de rapport de Thucydides. Le fichier pom.xml résultant devrait ressembler à quelque chose comme ça (avec les dernières versions stables des dépendances au moment de l'écriture de ceci):

  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
  
    <groupId>fr.poum</groupId>
    <artifactId>thutoweb</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
  
    <name>thutoweb</name>
    <url>http://maven.apache.org</url>
  
    <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <thucydides.version>0.7.3</thucydides.version>
    </properties>
  
      <dependencies>
          <dependency>
              <groupId>junit</groupId>
              <artifactId>junit</artifactId>
              <version>4.10</version>
              <scope>test</scope>
          </dependency>
          <dependency>
              <groupId>org.hamcrest</groupId>
              <artifactId>hamcrest-all</artifactId>
              <version>1.1</version>
              <scope>test</scope>
          </dependency>
          <dependency>
              <groupId>net.thucydides</groupId>
              <artifactId>thucydides-junit</artifactId>
              <version>${thucydides.version}</version>
             <scope>test</scope>
          </dependency>
          <dependency>
              <groupId>org.slf4j</groupId>
              <artifactId>slf4j-simple</artifactId>
              <version>1.6.4</version>
              <type>pom</type> <! pas sur ca >
          </dependency>
      </dependencies>
  
      <build>
          <plugins>
              <plugin>
                  <groupId>net.thucydides.maven.plugins</groupId>
                  <artifactId>maven-thucydides-plugin</artifactId>
                  <version>${thucydides.version}</version>
              </plugin>
          </plugins>
      </build>
  
  </project>

Une fois ceci fait, lancer

  $ mvn package

pour vous assurer que les configurations de pom.xml et settings.xml sont correctes. Attendez-vous à télécharger quelques (euphémisme !) dépendances....

Alternative

On pourra aussi utiliser un archetype pour se simplifier la tâche:

  $ mvn archetype:generate

et choisir: net.thucydides.thucydides-easyb-archetype (on pourra filter avec 'thucydides' pour le trouver plus facilement) Prendre la dernière version (0.31). Seul bémol, les versions dans le pom.xml paraissent un peu anciennes.

A noter qu'il existe aussi un second archetype pour utiliser easyb.

Ecrire votre premier test de recette

Commençons par écrire un test de recette JUnit. Pour commencer, nous allons décrire l'application dans les termes évoqués précédemment à l'aide de classe Java très légère, ce qui permettra de les refactoriser plus facilement:

Ouvrez ce projet dans votre IDE préféré (si ce n'est pas déjà fait) et créez une nouvelle classe appelée Application. Nous la placerons dans un package spécial appelé "exigences" (mais libre à vous de faire autrement). Cette classe contient la liste des "fonctionnalités" ("épopées" ou "epics") qui constituent l'application ainsi que les histoires associées à chaque fonctionnalité. Ces histoires ne sont pas les tests eux-même - mais plutôt elles sont utilisées pour modéliser les exigences de l'application. Plusieurs tests peuvent se référer à la même histoire. Plaçons cette classe dans src/java/fr/poum/exigences/Application.java:

  package fr.poum.exigences;
  
  import net.thucydides.core.annotations.Feature;
  
  public class Application {
      @Feature
      public class Rechercher {
          public class RechercherParMotClef {}   
          public class RechercherUneCitation{}
      }
  
      @Feature
      public class Contribuer {
          public class AjouterUnNouvelArticle {}
          public class ModifierUnArticleExistant {}
      }
  }

Maintenant, créons une nouvelle classe de test dans le package de votre choix appelée RechercherParMotClefStoryTest.java. A cette étape, nous allons ajouter 2 tests en attente ("pending"). Les tests en attente sont des tests qui ont été spécifiés et automatisés (i.e. écrits comme un test exécutable) mais pas encore implémentés.

  @RunWith(ThucydidesRunner.class) 
  @Story(Application.Rechercher.RechercherParMotClef.class)
  public class RechercherParMotClefStoryTest {
  
      @Managed
      public WebDriver webdriver;
  
      @ManagedPages(defaultUrl = "http://www.wikipedia.fr")
      public Pages pages;
  
      @Pending @Test
      public void rechercher_avec_un_mot_clef_non_ambigu_doit_afficher_l_article_correspondant() {
      }
  
      @Pending @Test
      public void rechercher_avec_un_mot_clef_ambigu_doit_afficher_la_page_de_resolution_d_ambiguite() {
      }
  }

La classe ThucydidesRunner indique qu'il s'agit d'un test Thucydides. Les annotations et les champs @Managed et @ManagedPages sont obligatoires pour prendre soin de nos objets page: nous verrrons celà plus loin....

Avant de continuer, lançons ces tests et voyons ce qu'il se passe. Vous pouvez les lancer directement depuis votre IDE - vous devriez obtenir 2 tests ignorés. Mais il y a mieux. Depuis la ligne de commande ou depuis votre IDE, lancez le but maven thucydides:aggregate, ex:

  $ mvn test thucydides:aggregate

Félicitations ! Vous venez juste de générer votre premier rapport Thucydides ! Allez dans le répertoire target/site/thucydides et ouvrez le fichier index.html. Ceci va ouvrir un tableau de bord avec un grand carré jaune. Ce carré jaune représente votre application: une seule fonctionnalité a été spécifiée à ce stade (la fonctionnalité 'Rechercher'), mais vous pouvez détailler cette fonctionnalité pour voir quels histoires et critères de validation ont été spécifiés. C'est un peu rasoir pour le moment, mais quand nous ajouterons davantage de fonctionnalités et de tests, cela devriendra plusd intéressant.

Ensuite, nous allons implémenter un de ces tests. Avec Thucydides, les tests sont découpés en plusieurs étapes (steps). Lors de la génération du rapport, chasue étape apparaît dans le rapport de test comme partie d'un test global. Les étapes sont simplement des méthodes annotées, sauvegardées dans des classes spéciales appelées "step librairies" ou bibliothèque d'étapes. Une bibliothèque d'étapes est une classe qui hérite de la classe ScenarioSteps.

La meilleur moyen de découvrir de quelles étapes et de quelles bibliothèques d'étapes vous avez besoin est de commencer à les exprimer dans vos tests et à les implémenter quand vous en avez besoin. Prenons le premier test: rechercher_avec_un_mot_clef_non_ambigu_doit_afficher_l_article_correspondant. Nous pourrions implémenter cela comme montré ici (notez que nous avons supprimé l'annotation @Pending, puis qu'il n'est plus en attente désormais):

  @Test
  public void rechercher_avec_un_mot_clef_non_ambigu_doit_afficher_l_article_correspondant() {
      utilisateurFinal.saisit_mot_clef("chats");
      utilisateurFinnal.doit_voir_l_article_avec_le_titre("Chat");
  }

Mais d'où vient cet utilisateurFinal ? Ceci sera notre bibliothèque d'étapes. Il est souvent commode d'organiser les étapes par acteur, car cela rend la lecture des tests plus fluide. Cela encourage également à donner des noms d'étapes plus lisibles ("saisit_mot_clef" et "soit_voir_l_article_avec_le_titre").

Les étapes sont conçues pour maintenir une couche d'abstraction/d'isolation salutaire entre ce que vos utilisateurs ont à faire et comment ils vont le faire. Aussi ces étapes haut niveau ne rentrent pas dans les détails techniques, ce qui est intentionnel. Les détails arriveront plus tard.

Ceci ne compile pas encore, aussi nous avons besoin d'ajouter un peu de chair aux choses. Créons une bibliothèque d'étapes appelée UtilisateurFinalEtapes:

  import net.thucydides.core.annotations.Step;
  import net.thucydides.core.pages.Pages;
  import net.thucydides.core.steps.ScenarioSteps;
  
  public class UtilisateurFinalEtapes extends ScenarioSteps {
  
      public UtilisateurFinalEtapes(Pages pages) {
          super(pages);
      }  
  
      @Step
      public void saisit_mot_clef(String motClef) {
      }
  
      @Step
      public void doit_voir_l_article_avec_le_titre(String titre) {
      }
  }

Notez que vous devez implémenter un constructeur de la forme montrée ici (l'objet Pages est une fabirque de classe que Thucydides utilise pour fournir des objets page aux tests et aux étapes de tests=. Les méthodes d'étape doivent également être annotées avec l'annotation @Step.

Ensuite, nous avons juste besoin d'ajouter une instance de la bibliothèque UtilisateurFinalEpapes, un champ public annoté avec l'annotation @Steps:

  @RunWith(ThucydidesRunner.class)
  @Story(Application.Rechercher.RechercherParMotClef.class)
  public class RechercherParMotClefStoryTest {
  
      @Managed
      public WebDriver webdriver;
  
      @ManagedPages(defaultUrl = "http://www.wikipedia.com")
      public Pages pages;
  
      @Steps
      public UtilisateurFinalEtapes utilisateurFinal;
  
      @Test
      public void rechercher_avec_un_mot_clef_non_ambigu_doit_afficher_l_article_correspondant() { 
          utilisateurFinal.cherche("chats");
          utilisateurFinal.doit_voir_l_article_avec_le_titre("Chat - Wikipedia, the free encyclopedia");                                            
      }
  
      @Pending @Test
      public void rechercher_avec_un_mot_clef_ambigu_doit_afficher_la_page_de_resolution_d_ambiguite() { 
      }
  }

Maintenant que nous avons défini nos étapes, nous devons les implémenter. C'est ici que les objets pages entrent en jeu. Les objets Page sont un pattern qui implique de modéliser votre interface utilisateur de façon à cacher les détails plus techniques (tels que la structure HTML de la page) dans une classe, en ne présentant que les fonctionnalités de la classe en terme relativement haut niveau. Mais avant d'ajouter un objet page, voyons ce dont nous avos besoin pour implémenter les étapes que nous avons définies précédemment.

D'abord, implémentons la méthode cherche(). Ceci est à quoi devrait ressembler le code:

  import net.thucydides.core.pages.Pages;
  import net.thucydides.core.steps.ScenarioSteps;
  
  public class UtilisateurFinalEtapes extends ScenarioSteps {
  
      public UtilisateurFinalEtapes(Pages pages) {
          super(pages);
      }
  
      @Step
      public void cherche(String motClef) {
          PageAccueil pageAccueil = getPages().currentPageAt(PageAccueil.class);
          pageAccueil.saisir_mot_clef(motClef);
      }
  }

La première ligne de la méthode cherche récupère un objet page auprès de la fabrique d'objets page Thucydide. Pour faire celà, vous utilisez la méthode currentPageAt et vous fournissez l'objet page que vous souhaitez obtenir. Thucydide va instancier cette page pour vous. (En fait, il fait un peu plus que celà, notamment le contrôle optionnel que vous êtes bien sur la bonne page, mais nous n'avons pas besoin de nous soucier de ça pour le moment). PageAccueil est le nom que j'ai choisi (absolument arbitrairement) pour représenter la page d'accueil Wikipedia. Et saisir_mot_clef semble un nom approprié pour saisir des mots clefs dans le champ de recherche de la page d'accueil.

Bien sûr, ceci ne va pas compiler, donc nous avons besoin d'ajouter un Objet Page. Maintenant que nous avons défini ce que nous voulons que l'objet page fasse, je peut demander à Eclipse de générer la plus grande partie de la classe pour moi simplement en utilisant la fonction "quick-fix":

Une fois que la classe a été générée, nous la faisons hériter de la classe Thucydide PageObjecT. Cette classe ajoute un tas de fonctions utiles que nous aurions du écrire nous-même autrement, donc c'est généralement une bonne idée. Ensuite, une fois que nous avons ajouté un peu de magie WebDriver, nous obtenons une classe comportant les lignes suivantes:

  import org.openqa.selenium.WebDriver;
  import org.openqa.selenium.WebElement;
  
  import net.thucydides.core.pages.PageObject;
  
  public class PageAccueil extends PageObject {
  
      private WebElement champSaisieRecherche;
  
      @FindBy(name="go")
      private WebElement boutonChercher;
  
      public PageAccueil(WebDriver driver) {
          super(driver);
      }
  
      public void saisit_mot_clef(String motClef) {
          champSaisieRecherche.sendKeys(motClef);
          boutonChercher.click();
      }
  }

Maintenant, essayons d'implémenter la seconde étape de test. Tout ceci requiers de contrôler le titre de la page, une fonctionnalité que la classe PageObject fournit nativement. Aussi, nous allons simplement utiliser une assertion standard Hamcrest pour contrôler la valeur sans qu'aucun autre travail complémentaire ne soit nécessaire:

  import com.wakaleo.webtests.wikipedia.pages.HomePage;
  import net.thucydides.core.annotations.Step;
  import net.thucydides.core.pages.Pages;
  import net.thucydides.core.steps.ScenarioSteps;
  import static org.hamcrest.MatcherAssert.assertThat;
  import static org.hamcrest.Matchers.is;
  
  public class UtilisateurFinaleEtapes extends ScenarioSteps {
  
      public UtilisateurFinalEtapes(Pages pages) {
          super(pages);
      }
  
      @Step
      public void cherche(String motClef) {
          PageAccueil pageAccueil = getPages().currentPageAt(PageAccueil.class);
          pageAccueil.saisit_mot_clef(motClef);
      }
  
      @Step
      public void doit_voir_l_article_avec_le_titre(String titre) {
          PageAccueil pageAccueile = getPages().currentPageAt(PageAccueil.class);
          assertThat(pageAccueil.getTitle(), is(titre));
      }
  }

Quand ceci est fait,supprimer l'annotation @Pending du premier test et relancer vos tests avec la ligne de commandes:

  $ mvn test thucydides:aggregate

Une fois que les tests ont été exécutés, ouvrez le fichier home.html dans le répertoire target/thucydides. Vous devriez maintenant voir un test vert (succès) et un test jaune (en attente). Si vous détaillez le test vert, vous devriez obtenir les étapes exécutées et les captures d'écran correspondantes.

Félicitations ! Vous avez maintenant exécuté vos premiers tests web Thucydide ! Comme exercice, essayez d'implémenter le second test. Le code source de cet exemple est disponible sur Github à https://github.com/thucydides-webtests/thucydides-demos.

Bien que cela requiert un peu plus de réflexion préalable, il y a de nombreux avantages à cette approche multi-tiers. Les étapes sont exprimées en termes métier et peuvent être montrées au responsable produit, aux gens de la qualité, aux utilisateurs finaux et ainsi de suite sans crainte de les noyer dans des détails techniques.

Les exigences haut-niveau, exprimées commes des exemples, ont tendance à moins changer que les détails d'implémentation bas niveau. Les modifications faites aux pages dans les objets page n'affectent pas les étapes de test et les tests qui utilisent ces objets pages, ce qui rend la maintenance plus facile.

La réutilisabilité est un autre avantage. Vous pouvez réutiliser les étapes, leur donner des paramètres, les réutiliser pour des tests dirigés par les données etc. Une fois encore, ceci facilite la maintenance à long terme.

Sources