Copyright © 2011 John Ferguson Smart.

Version en ligne publiée par Wakaleo Consulting.

Ce travail est sous licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Pas de Modification 3.0 United States Pour plus d’information sur cette licence, voir creativecommons.org/licenses/by-nc-nd/3.0/fr/.

Java™ et toutes les marques et logos basés sur Java sont des marques commerciales ou marques commerciales enregistrées de Sun Microsystems, Inc., aux Etats-Unis et autres pays.

Eclipse™ est une marque commerciale de Eclipse Foundation, Inc., aux Etats-Unis et autres pays.

Apache et le logo plume Apache sont des marques commerciales de la Apache Software Foundation.

Plusieurs des appellations utilisées par les fabricants et les vendeurs pour désigner leurs produits sont revendiqués comme marques commerciales. Quand ces appellations apparaissent dans ce livre, et quand Wakaleo Consulting était au fait de cette revendication de marque commerciale, les appellations ont été affichées en majuscules ou avec des initiales en majuscule.

Bien que toutes les précautions ont été prises dans la préparation de ce livre, l'éditeur et les auteurs déclinent toute responsabilité pour les erreurs ou omissions, ou pour les dommages résultant de l’utilisation des informations fournies ci-après.

2. Introduction à Thucydides

Thucydides (Tou-saille-didz) est un outil conçu pour faciliter l'écriture des tests de recette et de non régression automatisés. Il offre des fonctionnalités pour faciliter l’organisation et la structuration de vos tests de recette en les reliant avec vos histoires utilisateur ou les fonctionnalités qu’ils testent. Quand les tests sont exécutés, Thucydides génère une documentation illustrée qui décrit comment l’application est utilisée en s’appuyant sur les histoires décrites par les tests.

Thucydides apporte une prise en charge robuste des tests web automatisés utilisant Selenium 2, bien qu’il puisse être utilisé efficacement pour des tests non web.

Thucydides était un historien grec connu pour ses analyses astucieuses et qui consignait rigoureusement les événements dont il était témoin ou auxquels il participait lui-même. De la même manière, le framework Thucydides observe et analyse vos tests de recette et consigne de façon détaillée leur exécution.

3. Concepts fondamentaux du test de recette et de non régression

Pour tirer le meilleur parti de Thucydides, il est utile de comprendre quelques principes de base sous-jacents au développement dirigé par les tests de recette (Acceptance Test Driven Development). Thucydides est communément utilisé à la fois pour les tests de recette automatisés et les tests de non régression et les principes abordés ici s’appliquent aux deux, à quelques variations mineures près.

Acceptance Test Driven Development, ou ATDD, est une forme avancée du développement dirigé par les tests (Test Driven Development ou TDD) dans lequel les critères de recette automatiseés - définis en collaboration avec les utilisateurs - pilotent et orientent le processus de développement. Ceci aide à s’assurer que tout le monde comprend quelles sont les fonctionnalités en cours de développement.

Un des points importants au sujet de l’ATDD est l’idée de la "Spécification par l’Exemple". Spécification par l’exemple fait référence à l’utilisation d’exemples relativement concrets pour illustrer comment un système doit fonctionner, par opposition à des spécifications écrites plus formellement.

Prenons un exemple. Dans de nombreux projets, les exigences sont exprimées à l’aide de simples histoires telles que ce qui suit:

De façon à obtenir de l'argent pour acheter une nouvelle voiture
En tant que propriétaire d'une voiture
Je veux vendre ma vieille voiture sur Internet

Si nous développons un site web de vente de voitures qui aide les gens à atteindre cet objectif, nous devrions typiquement définir une série de critères de recette pour donner corps à notre histoire. Par exemple, nous pourrions avoir les critères suivants dans notre liste de critères de recette:

  • Le propriétaire de voiture peut publier une annonce de vente en ligne standard

  • Le propriétaire de voiture peut publier une annonce de vente en ligne premium

  • L’annonce voiture peut afficher la marque, le modèle et l’année de la voiture

et ainsi de suite.

Un testeur planifiant les tests pour ces critères de recette peut concevoir un plan de tests décrivant la façon dont il entend tester ces critères de recette. Pour le premier critère, il peut commencer par un plan de haut niveau tel que ce qui suit:

  • Aller à la section des annonces voiture et choisir de poster une annonce voiture standard

  • Saisir les informations relatives à la voiture

  • Choisir les options de publication

  • Prévisualiser l’annonce

  • Saisir les informations de paiement

  • Visualiser la confirmation de l’annonce

Chacune de ces étapes peut nécessiter d'être décomposée en étapes plus fines.

  • Saisir les informations relatives à la voiture

    • Saisir le fabriquant, le modèle et l’année de la voiture

    • Choisir les options

    • Ajouter des photos

    • Saisir une description

Ces étapes sont souvent étoffées avec plus de détails concrets:

  • Saisir les informations relatives à la voiture

    • Créer une annonce pour une Mitsubishi Pajero de 2006

    • Ajouter Air conditionné et lecteur CD

    • Ajouter trois photos

    • Saisir une description

Selon nos objectifs, les tests de non régression peuvent être définis comme des tests de bout en bout qui assurent que l’application se comporte comme attendu et qui continuera à se comporter comme attendu dans les prochaines livraisons. Autant les tests de recette sont définis très tôt dans le processus, avant le début du développement, autant les tests de non régression implique l’existence du système. A par cela, les étapes nécessaires à la définition et à l’automatisation des tests sont très semblables.

Maintenant différentes parties prenantes du projet seront impliquées à différents niveaux de détail. Certains, tels que les gestionnaires de projet et les gestionnaires en général, ne seront intéressés que par les fonctionnalités qui fonctionnent et par celles qui doivent être réalisées. D’autres, tels que les analystes métier et les qualiticiens, seront intéressés par la façon détaillée dont les scénarios de recette sont implémentés, éventuellement en allant jusqu’aux écrans.

Thucydides vous aide à structurer vos tests de recette automatisés en étapes et sous-étapes comme illustré ci-dessous. Ceci a tendance à rendre les tests plus clairs, plus flexibles et plus faciles à maintenir. De plus, quand les tests sont exécutés, Thucydides produit des rapports illustrés en style narratif comme dans [fig-test-report].

figs/test-report.png
Figure 1. Un rapport de test généré par Thucydides

Quand vient le moment d’implémenter les tests eux-même, Thucydides apporte également de nombreuses fonctionnalités pour rendre ceci plus facile, plus rapide et plus propre de façon à écrire des tests clairs et maintenables. Ceci est particulièrement vrai pour les tests web automatisés utilisant Selenium 2, mais Thucydides répond aussi bien aux besoins des tests non-web. Thucydides fonctionne actuellement avec JUnit et easyb - l’intégration avec d’autres framework BDD est en cours.

4. Débuter avec Thucydides

4.1. Créer un nouveau projet Thucydides

La façon la plus simple de démarrer un nouveau projet Thucydides est d’utiliser l’archétype Maven. Deux archétypes sont actuellement disponibles: un pour utiliser Thucydides avec JUnit, et l’autre si vous voulez également écrire vos tests de recette (ou une partie d’entre eux) en utilisant easyb. Dans cette section, nous créerons un nouveau projet Thucydides en utilisant l’archétype Thucydides, puis nous aborderons les fonctionnalités essentielles de ce projet.

En ligne de commandes, vous pouvez exécuter mvn archetype:generate puis choisir l’archétype net.thucydides.thucydides-easyb-archetype dans la liste des archétypes proposés. Ou bien, vous pouvez utiliser votre IDE préféré pour générer un nouveau projet Maven en utilisant un archétype.

$ mvn archetype:generate
...
Define value for property 'groupId': : com.mycompany
Define value for property 'artifactId': : webtests
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  com.mycompany: :
Confirm properties configuration:
groupId: com.mycompany
artifactId: webtests
version: 1.0-SNAPSHOT
package: com.mycompany
 Y: :
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2:33.290s
[INFO] Finished at: Fri Oct 28 07:20:41 NZDT 2011
[INFO] Final Memory: 7M/81M
[INFO] ------------------------------------------------------------------------

Ceci va créer un projet Thucydides simple complété d’un objet page, d’une bibliothèque d'étapes et de deux cas de tests, l’un utilisant JUnit et l’autre easyb. Les tests effectifs sont exécutés sur le dictionnaire en ligne Wiktionary.org. Avant d’aller plus loin, faisons un tour dans notre projet. Mais avant, nous devons ajouter net.thucydides.maven.plugins à nos groupes de plugin dans notre fichier settings.xml:

<settings>
   <pluginGroups>
       <pluginGroup>net.thucydides.maven.plugins</pluginGroup>
       ...
      </pluginGroups>
  ...
</settings>

Ceci va nous permettre d’appeler le plugin Maven thucydides à partir de la ligne de commandes sous la forme abrégée montrée ici. Maintenant, allons dans le répertoire généré pour notre projet, lançons les tests et générons le rapport:

$ mvn test thucydides:aggregate

Ceci doit lancer des tests web et générer un rapport dans le répertoire target/site/thucydides (ouvrez le fichier index.html). Vous devriez voir un rapport tel que celui-ci [fig-aggregate-report].

figs/aggregate-report.png
Figure 2. Un rapport global généré par Thucydides

Si vous descendez dans les rapports de tests individuels, vous verrez un récit illustré pour chaque test comme celui montré ici [fig-test-report]

Maintenant les détails. La structure projet est montrée ici:

+ src
   + main
      + java
         + com.mycompany.pages
            - HomePage.java

   + test
      + java
         + com.mycompany.pages
            + requirements
               - Application.java
            + steps
               - EndUserSteps.java
            - LookupADefinitionStoryTest.java

      + stories
         + com.mycompany
            - LookupADefinition.story

Ce projet est conçu pour fournir un point de départ à vos tests de recette Thucydides et pour illustrer certaines des fonctionnalités de base. Les tests sont fournis dans deux styles: easyb et JUnit. easyb est une bibliothèque BDD (Behaviour Driven Development) reposant sur Groovy qui fonctionne bien pour ce type de tests. L’histoire exemple easyb se trouve dans le fichier LookupADefinition.story et ressemble à quelque chose comme ça:

using "thucydides"

thucydides.uses_default_base_url "http://en.wiktionary.org/wiki/Wiktionary:Main_Page"
thucydides.uses_steps_from EndUserSteps
thucydides.tests_story SearchByKeyword

scenario "Looking up the definition of 'apple'", {
    given "the user is on the Wikionary home page", {
        end_user.is_the_home_page()
    }
    when "the end user looks up the definition of the word 'apple'", {
        end_user.looks_for "apple"
    }
    then "they should see the definition of 'apple", {
       end_user.should_see_definition_containing_words "A common, round fruit"
    }
}

Un coup d’oeil rapide à cette histoire montre qu’elle se rapporte à un utilisateur cherchant la définition du mot apple. Seul le "quoi" est exprimé à ce niveau - les détails sont masqués dans les étapes du test, voire plus profondément, dans les objets page.

Si vous préférez des tests en pur Java, l'équivalent JUnit se trouve dans le fichier LookupADefinitionStoryTest.java:

@Story(Application.Search.SearchByKeyword.class)
@RunWith(ThucydidesRunner.class)
public class LookupADefinitionStoryTest {

    @Managed(uniqueSession = true)
    public WebDriver webdriver;

    @ManagedPages(defaultUrl = "http://en.wiktionary.org/wiki/Wiktionary:Main_Page")
    public Pages pages;

    @Steps
    public EndUserSteps endUser;

    @Issue("#WIKI-1")
    @Test
    public void looking_up_the_definition_of_apple_should_display_the_corresponding_article() {
        endUser.is_the_home_page();
                endUser.looks_for("apple");
        endUser.should_see_definition_containing_words("A common, round fruit");

    }
}

Comme vous pouvez le voir, c’est un petit peu plus technique mais cela reste à très haut niveau.

Les bibliothèques d'étapes contiennent l’implémentation de chacune des étapes utilisées dans les tests de haut niveau. Pour des tests complexes, ces étapes peuvent à leur tour faire appel à d’autres étapes. La bibliothèque d'étapes utilisée dans cet exemple peut être trouvée dans EndUserSteps.java:

public class EndUserSteps extends ScenarioSteps {

        public EndUserSteps(Pages pages) {
                super(pages);
        }

    @Step
    public void searches_by_keyword(String keyword) {
        enters(keyword);
        performs_search();
    }

        @Step
        public void enters(String keyword) {
        onHomePage().enter_keywords(keyword);
        }

    @Step
    public void performs_search() {
        onHomePage().starts_search();
    }

    private HomePage onHomePage() {
        return getPages().currentPageAt(HomePage.class);
    }

    @Step
        public void should_see_article_with_title(String title) {
        assertThat(onHomePage().getTitle(), is(title));
        }

    @Step
    public void is_on_the_wikipedia_home_page() {
        onHomePage().open();
    }
}

Les objets Page sont un moyen d’encapsuler les détails d’implémentation d’une page donnée. Selenium 2 possède un particulièrement bon support pour les objets page et ThucydidesRunner en tire parti. L’objet page d’exemple se trouve dans la classe HomePage.java class:

@DefaultUrl("http://en.wiktionary.org/wiki/Wiktionary:Main_Page")
public class SearchPage extends PageObject {

    @FindBy(name="search")
        private WebElement searchInput;

        @FindBy(name="go")
        private WebElement searchButton;

        public SearchPage(WebDriver driver) {
                super(driver);
        }

        public void enter_keywords(String keyword) {
                searchInput.sendKeys(keyword);
        }

    public void starts_search() {
        searchButton.click();
    }

    public List<String> getDefinitions() {
        WebElement definitionList = getDriver().findElement(By.tagName("ol"));
        List<WebElement> results = definitionList.findElements(By.tagName("li"));
        return convert(results, new ExtractDefinition());
    }

    class ExtractDefinition implements Converter<WebElement, String> {
        public String convert(WebElement from) {
            return from.getText();
        }
    }
}

La pièce finale du puzzle est la classe Application.java qui est un moyen de représenter la structure de vos exigences sous forme Java de telle sorte que vos tests easyb et JUnit puissent être reliés aux exigences qu’ils testent:

        public class Application {
            @Feature
            public class Search {
                public class SearchByKeyword {}
                public class SearchByAnimalRelatedKeyword {}
                public class SearchByFoodRelatedKeyword {}
                public class SearchByMultipleKeywords {}
                public class SearchForQuote{}
            }

            @Feature
            public class Backend {
                public class ProcessSales {}
                public class ProcessSubscriptions {}
            }

            @Feature
            public class Contribute {
                public class AddNewArticle {}
                public class EditExistingArticle {}
            }
        }

C’est ce qui permet à Thucydides de générer un rapport global concernant les fonctionnalités et les histoires.

Dans les sections suivantes, nous verrons plus en détails les différents aspects de l'écriture des tests automatiques avec Thucydides.

5. Ecrire des tests de recette avec Thucydides

Dans cette section, nous examinons plus en détail les choses dont nous avons besoin pour écrire nos tests de recette ou de non régression en utilisant Thucydides. Nous allons également présenter une approche générale pour écrire des tests de recette web qui a bien fonctionné pour nous par le passé.

  1. Définir et organiser les exigences ou les histoires utilisateur que vous avez besoin de tester

  2. Ecrire des tests haut niveau en attente pour les critères de recette

  3. Choisir un test à implémenter et le diviser en petites étapes de haut niveau (typiquement entre 3 et 7)

  4. Implémenter ces étapes, soit en les divisant en d’autres étapes, soit en accédant à des objets page.

  5. Implémenter toutes les méthodes d’objet Page que vous avez découvertes.

Note

Ces étapes ne doivent pas être vues comme une approche linéaire ou en cascade. En effet, le processus est habituellement assez incrémental avec des exigences ajoutées à la classe Application quand elles sont nécessaires, et des tests en attente utilisés pour définir les tests avant qu’ils ne soient complétés.

5.1. Organiser vos exigences

Pour tirer le meilleur de vos tests automatisés dans Thucydides, vous devez dire à Thucydides quelles fonctionnalités de votre application vous testez dans chaque test. Bien que cette étape soit facultative, elle est fortement recommandée.

La version actuelle de Thucydides utilise une organisation simple à trois niveaux pour structurer les tests de recette en morceaux plus facilement gérables. Au plus haut niveau, une application est divisée en fonctionnalités (feature), qui est une fonction de haut niveau ou un groupe de fonctions en rapport. Une fonctionnalité contient un certain nombre d’histoires (stories, correspondant aux histoires utilisateur, cas d’utilisation, etc.). Chaque histoire est validée par un certain nombre d’exemples, ou critères d’acceptation, qui sont automatisés sous forme de tests web (parfois appelés scénarios). Chaque test, à son tour, est implémenté en utilisant un certain nombre d'étapes.

Bien sûr cette structure et ces termes sont principalement des commodités pour permettre une vision haut niveau de vos tests de recette. Cependant, ce type d’abstraction à 3 niveaux semble être assez commune.

Dans la version actuelle de Thucydides, vous définissez cette structure à l’intérieur du code de test sous forme de classes Java (très légères)
[Les futures versions de Thucydides offriront d’autres façons de définir vos exigences utilisateur.]
. Ceci facilite la refactorisation et le renommage des histoires utilisateur et des fonctionnalités à l’intérieur des tests, et donne un point de référence central dans la suite de tests illustrant quelles fonctionnalités sont en cours de test. Un exemple simple est donné ici. La classe Application est simplement un moyen pratique de regrouper dans un seul fichier les fonctionnalités et les histoires utilisateur. Les fonctionnalités sont repérées avec l’annotation @Feature. Les histoires utilisateur sont déclarées sous forme de classes internes à l’intérieur d’une classe @Feature.

public class Application {

    @Feature
    public class ManageCompanies {
        public class AddNewCompany {}
        public class DeleteCompany {}
        public class ListCompanies {}
    }

    @Feature
    public class ManageCategories {
        public class AddNewCategory {}
        public class ListCategories {}
        public class DeleteCategory {}
    }

    @Feature
    public class ManageTags {
        public class DisplayTagCloud {}
    }

    @Feature
    public class ManageJobs {}

    @Feature
    public class BrowseJobs {
        public class UserLookForJobs {}
        public class UserBrowsesJobTabs {}
    }
}

6. Définir des tests de haut niveau

Il y a deux approches pour automatiser les tests de recette ou de non régression avec Thucydides. Les deux impliquent d’implémenter les tests sous forme d’une séquence d'étapes de très haut niveau puis d’implémenter ces étapes en descendant dans les détails jusqu'à atteindre les objets page. La différence réside dans le langage utilisé pour implémenter les tests de haut niveau. Des outils tels que easyb se concentrent davantage sur la communication avec les non-développeurs et permettent aux tests de haut niveau d'être exprimés plus facilement en termes métier. D’un autre côté, les développeurs trouvent souvent plus confortable de travailler directement avec JUnit, donc, si la communication avec des parties prenantes non techniciens n’est pas une priorité, cette façon de faire peut être envisagée favorablement.

Dans la version actuelle de Thucydides, vous pouvez écrire vos tests en utilisant easyb (pour une approche davantage dans le style BDD) ou en JUnit en utilisant Java ou un autre langage JVM (Groovy est un choix populaire). D’autres outils BDD seront gérés dans de futures versions. Nous aborderons les deux ici, mais vous pouvez utiliser celle avec laquelle vous et votre équipe êtes le plus à l’aise.

6.1. Définir des tests haut niveau en easyb

Easyb (http://easyb.org) est un outil BDD basé sur Groovy. Il facilite l'écriture d’histoires et de scénarios légers en utilisant la structuration classique du style BDD "given-when-then" ("étant donné-lorsque-alors") et en les implémentant en Groovy. Le plugin easyb de Thucydides est conçu pour faciliter l'écriture des tests Thucydides en utilisant easyb.

6.1.1. Ecrire une histoire easyb en attente

Avec easyb, vous écrivez des tests (appelés "scenarios") qui, lorsqu’on utilise Thucydides, correspondent aux critères de recette automatisée. Les tests sont groupés en "histoires" ou "stories" - chaque histoire possède son propre fichier.

Les scénarios sont d’abord écrits comme "pending" (en attente). Ce sont juste des aperçus haut niveau, décrivant un ensemble de critères d’acceptation pour une histoire donnée et selon la structuration "given-when-then".

Quand les tests sont exécutés, les scénarios en attente sont ignorés. Cependant, ils apparaissent dans les rapports, de telle sorte que vous savez quelles fonctionnalités doivent encore être implémentées. Un exemple de la façon dont les scénarios en attente apparaissent dans un rapport Thucydides figure dans [fig-story-results-pending].

figs/story-results-pending.png
Figure 3. Les tests en attente sont affichés avec l’icone calendrier

Voici un exemple d’histoire easyb en attente utilisant Thucydides:

using "thucydides"

import net.thucydides.demos.jobboard.requirements.Application.ManageCategories.AddNewCategory

thucydides.tests_story AddNewCategory

scenario "The administrator adds a new category to the system",
{
        given "a new category needs to be added to the system"
        when "the administrator adds a new category"
        then "the system should confirm that the category has been created"
        and "the new category should be visible to job seekers"
}

scenario "The administrator adds a category with an existing code to the system",
{
        given "the administrator is on the categories list page"
        when "the user adds a new category with an existing code"
        then "an error message should be displayed"
}

Examinons cette histoire partie par partie. D’abord, vous devez déclarer que vous utilisez Thucydides. Vous faites cela en utilisant le mot clef easyb:

using "thucydides"

Ceci va, entre autres choses, injecter l’objet thucydides dans le contexte de votre histoire de telle sorte que vous pouvez configurer Thucydides pour exécuter votre histoire correctement.

Ensuite, vous devez dire à Thucydides quelle histoire vous être en train de tester. Vous faites cela en référençant l’une des classes d’histoire définies précédemment. C’est ce que nous faisons ici:

import net.thucydides.demos.jobboard.requirements.Application.ManageCategories.AddNewCategory

thucydides.tests_story AddNewCategory

Le reste de l’histoire easyb est juste une série de scénarios en attente easyb habituels. Pour le moment, il n’y a aucune implémentation de telle sorte qu’ils apparaissent en attente ("pending") dans les rapports:

scenario "The administrator adds a new category to the system",
{
        given "a new category needs to be added to the system"
        when "the administrator adds a new category"
        then "the system should confirm that the category has been created"
        and "the new category should be visible to job seekers"
}

scenario "The administrator adds a category with an existing code to the system",
{
        given "the administrator is on the categories list page"
        when "the user adds a new category with an existing code"
        then "an error message should be displayed"
}

Typiquement, vous déclarez de nombreuses histoires en attente, de préférence en collaboration avec le propriétaire du produit ou les analystes métier au début d’une itération. Ceci vous donne une bonne idée de quelles histoires doivent être implémentées dans une itération donnée ainsi qu’une idée de la complexité relative de chaque histoire.

6.1.2. Implémenter des histoires easyb

L'étape suivante consiste à implémenter vos histoires. Voyons une version implémentée du premier de ces scénarios:

using "thucydides"

import net.thucydides.demos.jobboard.requirements.Application.ManageCategories.AddNewCategory
import net.thucydides.demos.jobboard.steps.AdministratorSteps
import net.thucydides.demos.jobboard.steps.JobSeekerSteps

thucydides.uses_default_base_url "http://localhost:9000"
thucydides.uses_steps_from AdministratorSteps
thucydides.uses_steps_from JobSeekerSteps
thucydides.tests_story AddNewCategory

def cleanup_database() {
    administrator.deletes_category("Scala Developers");
}

scenario "The administrator adds a new category to the system",
{
    given "a new category needs to be added to the system",
    {
      administrator.logs_in_to_admin_page_if_first_time()
      administrator.opens_categories_list()
    }
    when "the administrator adds a new category",
    {
       administrator.selects_add_category()
       administrator.adds_new_category("Scala Developers","SCALA")
    }
    then "the system should confirm that the category has been created",
    {
        administrator.should_see_confirmation_message "The Category has been created"
    }
    and "the new category should be visible to job seekers",
    {
        job_seeker.opens_jobs_page()
        job_seeker.should_see_job_category "Scala Developers"
    }
}

De nouveau, décomposons ceci. Dans la première partie, nous importons les classes que nous avons besoin d’utiliser:

using "thucydides"

import net.thucydides.demos.jobboard.requirements.Application.ManageCategories.AddNewCategory
import net.thucydides.demos.jobboard.steps.AdministratorSteps
import net.thucydides.demos.jobboard.steps.JobSeekerSteps

Ensuite, nous déclarons l’URL de base par défaut à utiliser pour les tests. Comme l’annotation équivalente dans les tests JUnit, celle-ci est utilisée pour les tests exécutés depuis l’IDE ou si aucune URL de base n’est définie en ligne de commande en utilisant le paramètre webdriver.base.url.

thucydides.uses_default_base_url "http://localhost:9000"

Nous avons également besoin de déclarer les bibliothèques d'étapes de test que nous allons utiliser. Nous le faisons en utilisant thucydides.uses_steps_from. Ceci va injecter une variable d’instance dans le contexte easyb pour chaque bibliothèque d'étape déclarée. Si le nom de classe de la bibliothèque d'étape finit en Steps (par exemple JobSeekerSteps), le nom de la variable sera le nom de la classe moins le suffixe Steps, converti en minuscules et sous-ligné (ex. "job_seeker"). Nous en apprendrons davantage sur l’implémentation des bibliothèques d'étapes plus loin.

thucydides.uses_steps_from AdministratorSteps
thucydides.uses_steps_from JobSeekerSteps
thucydides.tests_story AddNewCategory

Enfin, nous implémentons le scénario. Notez que, puisqu’il s’agit de Groovy, nous pouvons déclarer des méthodes d’outillage pour faciliter la préparation et le nettoyage de l’environnement de test selon les besoins:

def cleanup_database() {
    administrator.deletes_category("Scala Developers");
}

Habituellement, l’implémentation invoque seulement des méthodes d'étape, comme illustré ici:

scenario "The administrator adds a new category to the system",
{
    given "a new category needs to be added to the system",
    {
      administrator.logs_in_to_admin_page_if_first_time()
      administrator.opens_categories_list()
    }
    when "the administrator adds a new category",
    {
       administrator.selects_add_category()
       administrator.adds_new_category("Scala Developers","SCALA")
    }
    then "the system should confirm that the category has been created",
    {
        administrator.should_see_confirmation_message "The Category has been created"
    }
    and "the new category should be visible to job seekers",
    {
        job_seeker.opens_jobs_page()
        job_seeker.should_see_job_category "Scala Developers"
        cleanup_database()
    }
}
Note

Si vous utilisez easyb avec Maven 3, il y a un bug Maven (Aether, pour être précis) qui peut se traduire par une erreur comme dans les lignes suivantes:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.171s
[INFO] Finished at: Fri Jul 08 15:33:03 EST 2011
[INFO] Final Memory: 5M/81M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project jobboard-webtests: Could not resolve dependencies for project net.thucydides.demos:jobboard-webtests:jar:1.0.0-SNAPSHOT: The following artifacts could not be resolved: commons-lang:commons-lang:jar:2.5, ant:ant:jar:1.6.5: Could not find artifact commons-lang:commons-lang:jar:2.5 -> [Help 1]

Ce problème a été corrigé dans Maven 3.0.4.

6.2. Définir de tests de haut niveau avec JUnit

Thucydides s’intègre facilement avec les tests JUnit 4 ordinaires en utilisant le lanceur de tests ThucydidesRunner et quelques annotations spécialisées. C’est une des manières les plus simples pour commencer avec Thucydides, et cela est très bien adapté aux tests de non régression où la communication et les clarifications avec les différentes parties prenantes n’est pas une exigence.

Voici un exemple de test web JUnit Thucydides:

@RunWith(ThucydidesRunner.class)
@Story(UserLookForJobs.class)
public class LookForJobsStory {

    @Managed
    public WebDriver webdriver;

    @ManagedPages(defaultUrl = "http://localhost:9000")
    public Pages pages;

    @Steps
    public JobSeekerSteps job_seeker;

    @Test
    public void user_looks_for_jobs_by_key_word() {
        job_seeker.opens_jobs_page();
        job_seeker.searches_for_jobs_using("Java");
        job_seeker.should_see_message("No jobs found.");
    }

    @Test
    public void when_no_matching_job_found_should_display_error_message() {
        job_seeker.opens_jobs_page();
        job_seeker.searches_for_jobs_using("unknownJobCriteria");
        job_seeker.should_see_message("No jobs found.");
    }

    @Pending @Test
    public void tags_should_be_displayed_to_help_the_user_find_jobs() {}

    @Pending @Test
    public void the_user_can_list_all_of_the_jobs_for_a_given_tag() {}

    @Pending @Test
    public void the_user_can_see_the_total_number_of_jobs_on_offer() {}

}

Examinons ceci section par section. La classe commence par l’annotation @RunWith pour indiquer qu’il s’agit d’un test Thucydides. Nous utilisons également l’annotation @Story pour indiquer quelle histoire utilisateur est testée (définie comme classe imbriquée des classes @Feature vues plus haut). Ceci est utilisé pour générer des rapports globaux.

@RunWith(ThucydidesRunner.class)
@Story(UserLookForJobs.class)
public class LookForJobsStory {
    ...

Ensuite arrivent deux annotations essentielles pour tous les tests web. Tout d’abord, votre cas de test a besoin d’un champ public Webdriver annoté avec @Managed. Ceci permet à Thucydides de prendre soin pour vous de l’ouverture et de la fermeture du pilote WebDriver et d’utiliser ce pilote dans les pages et les étapes de test quand les tests sont exécutés:

    @Managed
    public WebDriver webdriver;

Le second champ essentiel est une instance de la classe Pages, annotée avec @ManagedPages. Il s’agit essentiellement d’une fabrique de page que Thucydides utilise pour vous fournir des objets page instanciés. L’attribut defaultUrl vous permet de définir une URL à utiliser à l’ouverture de vos pages si aucune autre URL de base n’a été définie. Ceci est utile pour les tests dans l’IDE:

    @ManagedPages(defaultUrl = "http://localhost:9000")
    public Pages pages;

Notez que ces annotations ne sont nécessaires que pour les tests web. Si votre test Thucydides n’utilise pas de tests web, vous pouvez les ignorer en toute sérénité.

Pour des tests haut niveau de recette ou de non régression, c’est une bonne habitude de définir les tests de haut niveau comme une suite d'étapes de haut niveau. Cela rendra vos tests plus lisibles et plus faciles à maintenir si vous déléguez les détails d’implémentation de votre test (le "comment") à des méthodes réutilisables d'étape. Nous verrons comment définir ces méthodes d'étape plus tard. Cependant, vous devez au minimum définir la classe dans laquelle les étapes seront définies, en utilisant l’annotation @Steps. Cette annotation demande à Thucydides d'être à l'écoute des appels de méthodes de cet objet et (pour les tests web) d’injecter l’instance WebDriver et la fabrique de page dans la classe Steps de telle sorte qu’elles puissent être utilisées dans les méthodes d'étapes.

    @Steps
    public JobSeekerSteps job_seeker;

6.2.1. Tests en attente

Les tests qui ne contiennent aucune étape sont considérés comme étant en attente (pending). Vous pouvez également forcer une étape à être ignorée (et signalée comme étant en attente) en utilisant l’annotation @Pending ou l’annotation @Ignore. Noter que le sens est légèrement différent: @Ignore indique que vous avez temporairement suspendu l’exécution d’un test tandis que @Pending signifie que le test a été spécifié mais pas encore implémenté. Ainsi, les deux tests suivants seront en attente:

@Test
public void administrator_adds_an_existing_company_to_the_system() {}

@Pending @Test
public void administrator_adds_a_company_with_an_existing_code_to_the_system() {
    steps.login_to_admin_page();
    steps.open_companies_list();
    steps.select_add_company();
    // More to come
}

Un test sera également considéré comme étant en attente si au moins l’une des étapes utilisées dans ce test est en attente. Pour qu’une étape soit en attente, elle doit être annotée avec @Pending.

6.2.2. Exécuter des tests dans une unique session du navigateur

Normalement, Thucydides ouvre une session du navigateur pour chaque test. Ceci permet de s’assurer plus facilement que chaque test est isolé et indépendant. Cependant, parfois, il est utile d'être capable d’exécuter des tests dans une même session du navigateur, en particulier pour des raisons de performance sur des écrans en lecture seule. Vous pouvez faire cela en utilisant l’attribut uniqueSession de l’annotation @Managed comme montré ci-dessous. Dans ce cas, le navigateur va s’ouvrir au début du cas de test et ne se fermera pas tant que tous les tests n’auront pas été exécutés.

@RunWith(ThucydidesRunner.class)
public class OpenStaticDemoPageSample {

    @Managed(uniqueSession=true)
    public WebDriver webdriver;

    @ManagedPages(defaultUrl = "classpath:static-site/index.html")
    public Pages pages;

    @Steps
    public DemoSiteSteps steps;

    @Test
    @Title("The user opens the index page")
    public void the_user_opens_the_page() {
        steps.should_display("A visible title");
    }

    @Test
    @Title("The user selects a value")
    public void the_user_selects_a_value() {
        steps.enter_values("Label 2", true);
        steps.should_have_selected_value("2");
    }

    @Test
    @Title("The user enters different values.")
    public void the_user_opens_another_page() {
        steps.enter_values("Label 3", true);
        steps.do_something();
        steps.should_have_selected_value("3");
    }
}

Si vous n’avez pas besoin de la gestion de WebDriver dans vos tests, vous pouvez omettre les annotations @Managed et @Pages, par exemple:

@RunWith(ThucydidesRunner.class)
@Story(Application.Backend.ProcessSales.class)
public class WorkWithBackendTest {

    @Steps
    public BackendSteps backend;

    @Test
    public void when_processing_a_sale_transation() {
        backend.accepts_a_sale_transaction();
        backend.should_the_update_mainframe();
    }
}

6.3. Exécuter Thucydides dans différents navigateurs

Thucydides gère tous les pilotes WebDriver de navigateurs, i.e. Firefox, Internet Explorer et Chrome ainsi que HTMLUnit. Par défaut, il utilisera Firefox. Cependant, vous pouvez passer outre cette option en utilisant la propriété système webdriver.driver. Pour l’utiliser en ligne de commandes, vous pouvez procéder comme suit:

$ mvn test -Dwebdriver.driver=iexplorer

Si vous n’utilisez pas Firefox par défaut, il vous sera utile de définir cette variable en tant que propriété dans votre fichier Maven pom.xml, par exemple:

<properties>
    <webdriver.driver>iexplorer</webdriver.driver>
</properties>

Cependant, pour que ceci fonctionne avec JUnit, vous devez passer cette propriété webdriver.driver à JUnit. JUnit s’exécute dans une JVM dédiée et n’aura donc pas connaissance des propriétés système définies dans le build Maven. Pour y remédier, vous devez la passer explicitement à JUnit en utilisant l’option de configuration systemPropertyVariables, par exemple:

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.7.1</version>
            <configuration>
                <systemPropertyVariables>
                    <webdriver.driver>${webdriver.driver}</webdriver.driver>
                </systemPropertyVariables>
            </configuration>
        </plugin>

6.4. Forcer l’utilisation d’un pilote spécifique dans un cas de test ou dans un test

L’annotation @Managed vous permet également d’indiquer le pilote que vous voulez utiliser pour un cas de test particulier, via l’attribut driver. Les valeurs gérées actuellement sont "firefox", "iexplorer", "chrome" et "htmlunit". L’attribut driver vous permet de surcharger le pilote par défaut du système pour des exigences spécifiques. Par exemple, le cas de test suivant s’exécutera sous Chrome, quelle que soit la valeur de la propriété système webdriver.driver utilisée:

@RunWith(ThucydidesRunner.class)
@Story(Application.Search.SearchByKeyword.class)
public class SearchByFoodKeywordStoryTest {

    @Managed(uniqueSession = true, driver="chrome")
    public WebDriver webdriver;

    @ManagedPages(defaultUrl = "http://www.google.co.nz")
    public Pages pages;

    @Steps
    public EndUserSteps endUser;

    @Test
    public void searching_by_keyword_pears_should_display_the_corresponding_article() {
        endUser.is_the_google_home_page();
            endUser.enters("pears");
        endUser.starts_search();
            endUser.should_see_article_with_title_containing("Pear");
    }

    @Test
    @WithDriver("firefox")
    public void searching_by_keyword_pineapples_should_display_the_corresponding_article() {
        endUser.is_the_google_home_page();
            endUser.enters("pineapples");
        endUser.starts_search();
        endUser.should_see_article_with_title_containing("Pineapple");
    }
}

Avec easyb, vous pouvez utiliser la directive use_driver comme illustré ici:

using "thucydides"
...
thucydides.uses_default_base_url "http://localhost:9000"
thucydides.uses_driver chrome

...

scenario "The administrator adds a new category to the system",
{
    given "a new category needs to be added to the system",
    {
      administrator.logs_in_to_admin_page_if_first_time()
      administrator.opens_categories_list()
    }
    when "the administrator adds a new category",
    {
       administrator.selects_add_category()
       administrator.adds_new_category("Scala Developers","SCALA")
    }
    then "the system should confirm that the category has been created",
    {
        administrator.should_see_confirmation_message "The Category has been created"
    }
    and "the new category should be visible to job seekers",
    {
        job_seeker.opens_jobs_page()
        job_seeker.should_see_job_category "Scala Developers"
    }
}

Avec JUnit, vous pouvez également utiliser l’annotation @WithDriver pour indiquer un pilote pour un test individuel. Ceci surchargera à la fois le pilote système et l’attribut pilote de l’annotation @Managed s’il y en a. Par exemple, le test suivant s’exécutera toujours avec Firefox:

        @Test
        @WithDriver("firefox")
        public void searching_by_keyword_pineapples_should_display_the_corresponding_article() {
            endUser.is_the_google_home_page();
            endUser.enters("pineapples");
            endUser.starts_search();
            endUser.should_see_article_with_title_containing("Pineapple");
        }

7. Implémenter des bibliothèques d'étapes

une fois que vous avez défini les étapes dont vous avez besoin pour décrire vos tests haut niveau, vous devez les implémenter. Dans un test web automatisé, les étapes de test représentent le niveau d’abstraction situé entre vos objets pages (qui sont conçues en terme d’actions que vous exécutez sur une page donnée) et les histoires de plus haut niveau (suites d’actions davantage orientées métier qui illustrent comment une histoire utilisateur donnée a été implémentée). Les étapes peuvent contenir d’autres étapes et sont incluses dans les rapports Thucydides. A chaque fois qu’une étape est exécutée, une capture d'écran est enregistrée et affichée dans le rapport.

7.1. Créer des bibliothèques d'étapes

Les étapes de test sont des méthodes Java classiques annotées avec @Step. Vous rangez les étapes et les groupes d'étapes dans des bibliothèques d'étapes. Une bibliothèque d'étapes n’est qu’une classe Java classique. Si vous exécutez des tests web, votre bibliothèque d'étapes devra soit posséder une variable membre Pages soit, plus simplement, hériter de la classe ScenarioSteps, par exemple:

public class JobSeekerSteps extends ScenarioSteps {
    public JobSeekerSteps(Pages pages) {
        super(pages);
    }


    @Step
    public void opens_jobs_page() {
        FindAJobPage page = getPages().get(FindAJobPage.class);
        page.open();
    }

    @Step
    public void searches_for_jobs_using(String keywords) {
        FindAJobPage page = getPages().get(FindAJobPage.class);
        page.look_for_jobs_with_keywords(keywords);

    }
}

Notez que les méthodes d'étapes peuvent attendre des paramètres. Ces paramètres passés à la méthode d'étape seront enregistrés et apparaîtront dans les rapports Thucydides, ce qui en fait une excellente technique pour rendre vos tests davantage maintenables et modulaires.

Les étapes peuvent également appeler d’autres étapes, ce qui est très utile pour des scénarios de tests plus compliqués. Le résultat sera la sorte de structure imbriquée que vous pouvez voir dans [fig-test-report].

8. Définir des objets Page

Si vous travaillez avec des tests web WebDriver, vous connaissez le concept de Page Objects ou Objets Page. Les objets Page sont une manière de masquer les détails d’implémentation d’une page web à l’intérieur d’une classe en n’exposant que les méthodes orientées métier en rapport avec cette page. C’est une excellente façon de rendre les tests web davantage maintenables.

Avec Thucydides, les objets page sont des objets page WebDriver habituels à la condition qu’ils possèdent un constructeur qui prenne un paramètre WebDriver. Toutefois, le PageObject de Thucydides fournit un certain nombre de méthodes utilitaires qui rendent plus commode l’utilisation des objets page, c’est pourquoi un objet page Thucydides hérite généralement de cette classe.

Voici un exemple simple:

@DefaultUrl("http://localhost:9000/somepage")
public class FindAJobPage extends PageObject {

    WebElement keywords;
    WebElement searchButton;

    public FindAJobPage(WebDriver driver) {
        super(driver);
    }

    public void look_for_jobs_with_keywords(String values) {
        typeInto(keywords, values);
        searchButton.click();
    }

    public List<String> getJobTabs() {
        List<WebElement> tabs = getDriver().findElements(By.xpath("//div[@id='tabs']//a"));
        return extract(tabs, on(WebElement.class).getText());
    }
}

La méthode typeInfo est un raccourci qui se contente d’effacer un champ et d’y entrer le texte indiqué. Si vous préférez un style davantage orienté API, vous pouvez également faire quelque chose comme ça:

@DefaultUrl("http://localhost:9000/somepage")
public class FindAJobPage extends PageObject {
        WebElement keywordsField;
        WebElement searchButton;

        public FindAJobPage(WebDriver driver) {
            super(driver);
        }

        public void look_for_jobs_with_keywords(String values) {
            **enter(values).into(keywordsField);**
            searchButton.click();
        }

        public List<String> getJobTabs() {
            List<WebElement> tabs = getDriver().findElements(By.xpath("//div[@id='tabs']//a"));
            return extract(tabs, on(WebElement.class).getText());
        }
}

8.1. Utiliser des pages dans une bibliothèque d'étapes

Lorsque vous avez besoin d’utiliser un objet page dans l’une de vos étapes, vous vous contentez d’en demander une à la fabrique Page (Page factory) qui vous le fournira, par exemple:

FindAJobPage page = getPages().get(FindAJobPage.class);

Si vous voulez être sûr que vous êtes sur la bonne page, vous pouvez utiliser la méthode currentPageAt(). Celle-ci va chercher dans la classe Page Object toutes les annotations @At présentes et, s’il y en a, vérifiera que l’URL actuelle correspond au motif d’URL indiqué dans l’annotation. Par exemple, lorsque vous l’appelez en utilisant currentPageAt(), l’objet page suivant va vérifier que l’URL actuelle est précisément http://www.apache.org.

@At("http://www.apache.org")
public class ApacheHomePage extends PageObject {
    ...
}

L’annotation @At gère également les jokers et les expressions rationnelles. L’objet page suivant correspondra à tout sous-domaine Apache:

@At("http://.*.apache.org")
public class AnyApachePage extends PageObject {
    ...
}

Toutefois, dans le cas général, vous serez davantage intéressé par ce qui vient après le nom d’hôte. Vous pouvez utiliser le mot clef spécial #HOST pour que tout nom de serveur convienne. C’est pour cette raison que l’objet page suivant correspondra à la fois à http://localhost:8080/app/action/login.form et à http://staging.acme.com/app/action/login.form. Les paramètres seront également ignorés, donc http://staging.acme.com/app/action/login.form?username=toto&password=oz correspondra également.

@At(urls={"#HOST/app/action/login.form"})
public class LoginPage extends PageObject {
   ...
}

8.2. Ouvrir une page

Un objet page est habituellement conçu pour fonctionner avec une page web donnée. Quand la méthode open() est invoquée, le navigateur va s’ouvrir sur l’URL par défaut de la page.

L’annotation @DefaultUrl indique l’URL que ce test doit utiliser quand il est exécuté de manière isolée (par exemple depuis votre IDE). Généralement, cependant, la partie hôte de l’URL par défaut sera remplacée par la propriété webdriver.base.url ce qui vous permet de définir l’URL de base pour tous vos tests et de faciliter l’exécution de vos tests sur différents environnements, simplement en changeant cette valeur de propriété. Par exemple, dans la classe de test ci-dessus, définir webdriver.base.url à https://staging.mycompany.com fera que la page s’ouvrira à l’URL https://staging.mycompany.com/somepage.

Vous pouvez également définir des URL nommées qui peuvent être utilisées pour ouvrir la page web, assorties facultativement de paramètres. Par exemple, dans le code suivant, nous définissons une URL nommée open.issue qui accepte un unique paramètre:

@DefaultUrl("http://jira.mycompany.org")
@NamedUrls(
  {
    @NamedUrl(name = "open.issue", url = "http://jira.mycompany.org/issues/{1}")
  }
)
public class JiraIssuePage extends PageObject {
    ...
}

Vous pouvez alors ouvrir cette page sur l’URL http://jira.mycompany.org/issues/ISSUE-1 comme illustré ici:

page.open("open.issue", withParameters("ISSUE-1"));

Vous pouvez également omettre totalement l’URL de base dans la définition de l’URL nommée en vous reposant sur les valeurs par défaut:

@DefaultUrl("http://jira.mycompany.org")
@NamedUrls(
  {
    @NamedUrl(name = "open.issue", url = "/issues/{1}")
  }
)
public class JiraIssuePage extends PageObject {
    ...
}

Et naturellement, vous pouvez faire des définitions multiples:

@NamedUrls(
  {
          @NamedUrl(name = "open.issue", url = "/issues/{1}"),
          @NamedUrl(name = "close.issue", url = "/issues/close/{1}")
  }
)

Vous ne devriez jamais essayer d’implémenter la méthode open() vous-même. En fait, elle est final. Si vous avez besoin que votre page fasse quelque chose au chargement, comme attendre l’apparition d’un élément dynamique, vous pouvez utiliser l’annotation @WhenPageOpens. Les méthodes de PageObject dotées de cette annotation seront appelées (dans un ordre quelconque) après que l’URL aura été ouverte. Dans cette exemple, la méthode open() ne rendra pas la main tant que l'élément web dataSection ne sera pas visible:

@DefaultUrl("http://localhost:8080/client/list")
    public class ClientList extends PageObject {

     @FindBy(id="data-section");
     WebElement dataSection;
     ...

     @WhenPageOpens
     public void waitUntilTitleAppears() {
         element(dataSection).waitUntilVisible();
     }
}

8.3. Travailler avec des éléments web

8.3.1. Vérifier si des éléments sont visibles

La méthode element de la classe PageObject offre une API souple et commode pour interagir avec les éléments web en fournissant certaines fonctionnalités additionnelles fréquemment utilisées qui ne sont pas fournies nativement par l’API WebDriver. Par exemple, vous pouvez vérifier qu’un élément est visible comme illustré ici:

public class FindAJobPage extends PageObject {

    WebElement searchButton;

    public boolean searchButtonIsVisible() {
        return element(searchButton).isVisible();
    }
    ...
}

Si le bouton n’est pas présent à l'écran, le test va attendre un petit moment au cas où il apparaîtrait du fait de quelque magie Ajax. Si vous ne souhaitez pas que le test se comporte de cette façon, vous pouvez utiliser la version plus rapide:

public boolean searchButtonIsVisibleNow() {
    return element(searchButton).isCurrentlyVisible();
}

Vous pouvez transformer ceci en affirmation en utilisant à la place la méthode shouldBeVisible():

public void checkThatSearchButtonIsVisible() {
    element(searchButton).shouldBeVisible();
}

La méthode lancera une erreur d’affirmation si le bouton de recherche n’est pas visible pour l’utilisateur final.

Si vous n'êtes pas satisfait d’exposer le fait que votre page possède un bouton de recherche dans vos méthodes d'étape, vous pouvez rendre les choses encore plus simples en ajoutant une méthode accesseur qui renvoie une WebElementFacade, comme illustré ici:

public WebElementFacade searchButton() {
    return element(searchButton);
}

Vos étapes contiendront alors du code tel que ce qui suit:

        searchPage.searchButton().shouldBeVisible();

8.3.2. Vérifier si des éléments sont activés

Vous pouvez également vérifier si un élément est activé ou non:

element(searchButton).isEnabled() element(searchButton).shouldBeEnabled()

Il existe également les méthodes négatives correspondantes:

element(searchButton).shouldNotBeVisible();
element(searchButton).shouldNotBeCurrentlyVisible();
element(searchButton).shouldNotBeEnabled()

Vous pouvez également contrôler des éléments qui sont présents sur la page mais qui ne sont pas visibles, par exemple:

element(searchButton).isPresent();
element(searchButton).isNotPresent();
element(searchButton).shouldBePresent();
element(searchButton).shouldNotBePresent();

8.3.3. Manipuler des listes déroulantes

Il existe également des méthodes d’aide pour les listes déroulantes. Supposons que vous avez la liste déroulante suivante sur votre page:

<select id="color">
    <option value="red">Red</option>
    <option value="blue">Blue</option>
    <option value="green">Green</option>
</select>

Vous pouvez écrire un objet page pour manipuler cette liste déroulante comme suit:

public class FindAJobPage extends PageObject {

        @FindBy(id="color")
        WebElement colorDropdown;

        public selectDropdownValues() {
            element(colorDropdown).selectByVisibleText("Blue");
            assertThat(element(colorDropdown).getSelectedVisibleTextValue(), is("Blue"));

            element(colorDropdown).selectByValue("blue");
            assertThat(element(colorDropdown).getSelectedValue(), is("blue"));

            page.element(colorDropdown).selectByIndex(2);
            assertThat(element(colorDropdown).getSelectedValue(), is("green"));

        }
        ...
}

8.3.4. Vérifier le focus

Vous pouvez savoir si un champ donné a le focus comme suit:

element(firstName).hasFocus()

Vous pouvez également attendre que des éléments apparaissent, disparaissent, deviennent actifs ou inactifs:

element(button).waitUntilEnabled()
element(button).waitUntilDisabled()

ou

element(field).waitUntilVisible()
element(button).waitUntilNotVisible()

8.3.5. Utiliser des sélecteurs XPath et CSS

Un autre moyen d’accéder à des éléments web consiste à utiliser une expression XPath ou CSS. Vous pouvez utiliser la méthode element avec une expression XPath pour faire cela plus facilement. Par exemple, imaginez que votre application web nécessite de cliquer sur un élément de liste contenant un code postal donné. Une façon de faire serait la suivante:

WebElement selectedSuburb = getDriver().findElement(By.xpath("//li/a[contains(.,'" + postcode + "')]"));
selectedSuburb.click();

Cependant, il est plus simple de faire comme ceci:

element(By.xpath("//li/a[contains(.,'" + postcode + "')]")).click();

8.4. Travailler avec des pages asynchrones

Les pages asynchrones sont celles dont les champs ou les données ne sont pas tous affichés quand la page est chargée. Parfois, vous devez attendre que certains éléments apparaissent ou disparaissent pour lancer vos tests. Thucydides fournit certaines méthodes pratiques dans la classe PageObject pour faciliter la gestion de tels scénarios. Elles sont principalement conçues pour être utilisées dans les méthodes métier de vos objets page, mais dans les exemples, nous les utiliserons par appel externe sur un PageObject pour la clarté de la démonstration.

8.4.1. Vérifier si un élément est visible

Pour WebDriver, il existe une distinction entre le fait qu’un élément soit présent à l'écran (i.e. dans le code source HTML) et qu’il soit rendu (i.e. visible pour l’utilisateur). Vous pouvez également avoir besoin de savoir si un élément est visible à l'écran . Vous pouvez le faire de deux façons. La première possibilité est d’utiliser la méthode isElementVisible qui renvoie une valeur booléenne indiquant si l'élément est rendu (visible pour l’utilisateur) ou pas:

assertThat(indexPage.isElementVisible(By.xpath("//h2[.='A visible title']")), is(true));

ou

assertThat(indexPage.isElementVisible(By.xpath("//h2[.='An invisible title']")), is(false));

La seconde possibilité est de décider activement que l'élément doit être visible:

indexPage.shouldBeVisible(By.xpath("//h2[.='An invisible title']");

Si l'élément n’apparaît pas immédiatement, vous pouvez attendre qu’il apparaisse:

indexPage.waitForRenderedElements(By.xpath("//h2[.='A title that is not immediately visible']"));

Vous pouvez également attendre qu’un élément disparaisse:

indexPage.waitForRenderedElementsToDisappear(By.xpath("//h2[.='A title that will soon disappear']"));

Pour simplifier, vous pouvez également utiliser les méthodes waitForTextToAppear et waitForTextToDisappear:

indexPage.waitForTextToDisappear("A visible bit of text");

S’il y a plusieurs textes différents qui peuvent apparaître, vous pouvez utiliser waitForAnyTextToAppear ou waitForAllTextToAppear:

indexPage.waitForAnyTextToAppear("this might appear","or this", "or even this");

Si vous devez attendre qu’un élément parmi plusieurs possibles apparaisse, vous pouvez également utiliser la méthode waitForAnyRenderedElementOf:

indexPage.waitForAnyRenderedElementOf(By.id("color"), By.id("taste"), By.id("sound"));

8.5. Exécuter du Javascript

Il y a des situations dans lesquelles vous pourriez trouver utile d’exécuter un peu de Javascript directement dans le navigateur pour que le travail soit fait. Vous pouvez utiliser la méthode evaluateJavascript() de la classe PageObject pour faire cela. Par exemple, vous pourriez avoir besoin de calculer une expression et d’utiliser le résultat dans vos tests. La commande suivante va évaluer le titre du document et le renvoyer au code Java appelant:

String result = (String) evaluateJavascript("return document.title");

Alternativement, vous pourriez simplement vouloir exécuter une commande Javascript localement dans le navigateur. Dans le code suivant, par exemple, nous positionnons le focus sur le champ de saisie firstname:

        evaluateJavascript("document.getElementById('firstname').focus()");

Et, si vous êtes familier avec JQuery, vous pouvez également faire appel aux expressions JQuery:

        evaluateJavascript("$('#firstname').focus()");

Ceci est souvent une stratégie utile si vous avez besoin de déclencher des événements tels que des survols de souris qui ne sont actuellement pas gérés par l’API WebDriver.

8.6. Envoi de fichiers

Envoyer des fichiers est facile. Les fichiers à envoyer peuvent soit être placés dans un emplacement codé en dur (pas bien) ou enregistrés dans le chemin d’accès (mieux). Voici un exemple simple:

public class NewCompanyPage extends PageObject {
    ...
    @FindBy(id="object_logo")
    WebElement logoField;

    public NewCompanyPage(WebDriver driver) {
        super(driver);
    }

    public void loadLogoFrom(String filename) {
        upload(filename).to(logoField);
    }
}

8.7. Utiliser des expressions de correspondance souples

Quand on écrit des tests de recette, on est souvent amené à exprimer des attentes relatives à des objets du domaine ou à des ensembles d’objets du domaine. Par exemple, si vous testez une fonctionnalité de recherche multi-critères, vous voulez savoir si l’application trouve les enregistrements que vous attendez. Vous pourriez être capable de faire cela d’une manière très précise (par exemple en sachant exactement les valeurs des champs que vous attendez) ou bien vous pourriez vouloir rendre vos tests davantage flexibles en exprimant les plages de valeurs qui seraient acceptables. Thucydides fournit quelques fonctionnalités qui facilitent l'écriture des tests de recette dans ce type de cas.

Dans le reste de cette section, nous étudierons quelques exemples basés sur des tests du site de recherche Maven Central (voir [maven-search-report]). Ce site vous permet de rechercher des artéfacts Maven dans le dépôt Maven et de consulter les détails d’un artefact donné.

figs/maven-search-report.png
Figure 4. La page des résultats de la page de recherche de Maven Central

Nous allons utiliser quelques tests imaginaires de non régression pour ce site afin d’illustrer comment les comparateurs (matchers) de Thucydides peuvent être utilisés pour écrire des tests plus expressifs. Le premier scénario que nous allons envisager consiste à simplement chercher un artéfact par son nom et à s’assurer que seuls les artéfacts correspondant à ce nom apparaissent dans la liste des résultats. Nous pourrions énoncer informellement ce critère de validation de la manière suivante:

  • Etant donné que le développeur est sur la page de recherche

  • Et que le développeur cherche l’artéfact appelé Thucydides

  • Alors le développeur doit voir au moins 16 artéfacts Thucydides, chacun doté d’un unique Id d’artéfact

Avec JUnit, un test Thucydides correspondant à ce scénario pourrait ressembler à celui-ci:

...
import static net.thucydides.core.matchers.BeanMatchers.the_count;
import static net.thucydides.core.matchers.BeanMatchers.each;
import static net.thucydides.core.matchers.BeanMatchers.the;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;

@RunWith(ThucydidesRunner.class)
public class WhenSearchingForArtifacts {

    @Managed
    WebDriver driver;

    @ManagedPages(defaultUrl = "http://search.maven.org")
    public Pages pages;

    @Steps
    public DeveloperSteps developer;

    @Test
    public void should_find_the_right_number_of_artifacts() {
        developer.opens_the_search_page();
        developer.searches_for("Thucydides");
        developer.should_see_artifacts_where(the("GroupId", startsWith("net.thucydides")),
                                             each("ArtifactId").isDifferent(),
                                             the_count(is(greaterThanOrEqualTo(16))));

    }
}

Voyons comment le test est implémenté dans cette classe. Le test should_find_the_right_number_of_artifacts() peut être explicité comme suit:

  1. Quand nous ouvrons la page de recherche

  2. Et que nous cherchons l’artéfact contenant le mot Thucydides

  3. Alors nous devrions voir une liste d’artéfacts pour lesquels chaque Group ID commence par "net.thucydides", chaque Artifact ID est unique et qu’il y a au moins 16 entrées de ce type d’affichées.

L’implémentation de ces étapes est illustrée ici:

...
import static net.thucydides.core.matchers.BeanMatcherAsserts.shouldMatch;

public class DeveloperSteps extends ScenarioSteps {

    public DeveloperSteps(Pages pages) {
        super(pages);
    }

    @Step
    public void opens_the_search_page() {
        onSearchPage().open();
    }

    @Step
    public void searches_for(String search_terms) {
        onSearchPage().enter_search_terms(search_terms);
        onSearchPage().starts_search();
    }

    @Step
    public void should_see_artifacts_where(BeanMatcher... matchers) {
        shouldMatch(onSearchResultsPage().getSearchResults(), matchers);
    }

    private SearchPage onSearchPage() {
        return getPages().get(SearchPage.class);
    }

    private SearchResultsPage onSearchResultsPage() {
        return getPages().get(SearchResultsPage.class);
    }
}

Les deux premières étapes sont implémentées par des méthodes relativement simples. Cependant, la troisième étape est plus intéressante. Regardons-la de plus près:

    @Step
    public void should_see_artifacts_where(BeanMatcher... matchers) {
        shouldMatch(onSearchResultsPage().getSearchResults(), matchers);
    }

Ici, nous passons un nombre arbitraire d’expression à la méthode. Ces expressions sont en fait des matchers, des instances de la classe BeanMatcher. Vous n’avez normalement pas à vous soucier de ce niveau de détail - vous créez ces expressions de correspondance en utilisant un ensemble de méthodes statiques fournies par la classe BeanMatcher. Aussi vous ne devriez typiquement passer que des expressions relativements lisibles telles que the("GroupId", startsWith("net.thucydides")) ou each("ArtifactId").isDifferent().

La méthode shouldMatch() de la classe BeanMatcherAsserts attend soit un unique objet Java, soit un ensemble d’objets Java et vérifie qu’au moins certains de ces objets correspondent aux contraintes indiquées par les matchers. Dans le cas du test web, ces objets sont typiquement des POJOs fournis par l’objet page pour représenter des objets du domaine ou des objets affichés à l'écran.

Il existe un certain nombre d’expressions différentes de matchers parmi lesquelles choisir. Le matcher le plus communément utilisé vérifie simplement la valeur d’un champ dans un objet. Par exemple, supposons que vous utilisez l’objet domaine montré ici:

     public class Person {
        private final String firstName;
        private final String lastName;

        Person(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public String getFirstName() {...}

        public String getLastName() {...}
    }

Vous pourriez écrire un test pour vous assurer que la liste des personnes contienne au moins une personne appelée "Bill" en utilisant la méthode statique "the", comme montré ici:

    List<Person> persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor"));

    shouldMatch(persons, the("firstName", is("Bill"))

Le second paramètre de la méthode the() est un matcher Hamcrest qui vous donne une grande marge de flexibilité dans vos expressions. Par exemple, vous pourriez également écrire ce qui suit:

    List<Person> persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor"));

    shouldMatch(persons, the("firstName", is(not("Tim"))));
    shouldMatch(persons, the("firstName", startsWith("B")));

Vous pouvez également passer des conditions multiples:

    List<Person> persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor"));

    shouldMatch(persons, the("firstName", is("Bill"), the("lastName", is("Oddie"));

Thucydides fournit également la classe DateMatchers qui vous permet d’appliquer les matchers Hamcrest aux objets Java standard Dates et aux Datetimes JodaTime. Les exemples de code suivant illustrent la façon dont cela peut être utilisé:

    DateTime january1st2010 = new DateTime(2010,01,01,12,0).toDate();
    DateTime may31st2010 = new DateTime(2010,05,31,12,0).toDate();

    the("purchaseDate", isBefore(january1st2010))
    the("purchaseDate", isAfter(january1st2010))
    the("purchaseDate", isSameAs(january1st2010))
    the("purchaseDate", isBetween(january1st2010, may31st2010))

Vous avez également parfois besoin de vérifier des contraintes qui s’appliquent à tous les éléments considérés. Le plus simple de ces cas de figure consiste à vérifier que toutes les valeurs prises par un champ particulier sont uniques. Vous pouvez faire cela en utilisant la méthode each():

    shouldMatch(persons, each("lastName").isDifferent())

Vous pouvez également vérifier que le nombre d'éléments qui correspondent est conforme à ce que vous attendiez. Par exemple, pour vérifier qu’il n’y a qu’une seule personne dont le prénom est Bill, vous pourriez faire cela:

     shouldMatch(persons, the("firstName", is("Bill"), the_count(is(1)));

Vous pouvez également vérifier les valeurs minimum et maximum en utilisant les méthodes min() et max(). Par exemple, si la classe Person possède une méthode getAge(), nous pourrions nous assurer que chaque personne a plus de 21 ans et moins de 65 en faisant ce qui suit:

     shouldMatch(persons, min("age", greaterThanOrEqualTo(21)),
                          max("age", lessThanOrEqualTo(65)));

Ces méthodes fonctionnent avec les objets Java normaux mais aussi avec Maps. C’est pourquoi le code suivant fonctionne également:

    Map<String, String> person = new HashMap<String, String>();
    person.put("firstName", "Bill");
    person.put("lastName", "Oddie");

    List<Map<String,String>> persons = Arrays.asList(person);
    shouldMatch(persons, the("firstName", is("Bill"))

L’autre chose sympathique avec cette approche est que les matchers s’intègrent harmonieusement avec les rapports Thucydides. Ainsi, quand vous utilisez la classe BeanMatcher comme paramètre de vos étapes de test, les conditions exprimées dans l'étape seront affichées dans le rapport du test, comme montré dans [fig-maven-search-report].