Développement dirigé par les tests : mise en pratique


précédentsommairesuivant

IV. Chapitre 3 - Tests fonctionnels

Dans ce chapitre, notre travail sera, d'une part de produire et livrer l'application sur un environnement d'intégration et, d'autre part d'exécuter les tests fonctionnels (appelés également tests utilisateur ou tests de cas d'utilisation).

IV-A. Préparation du livrable

La préparation d'un livrable consiste à empaqueter l'application afin qu'elle puisse être installée dans un environnement cible (intégration, recette, etc.). Cela peut être un exécutable Windows ou Unix, un fichier jar, ear ou war. Dans notre cas, nous allons modifier notre projet pour produire un livrable de type application web (war).

IV-A-1. Mise à jour du pom

Nous allons modifier les propriétés suivantes.

  • Le packaging sera de type war
  • Le nom final lors du build sera mynotes (cela évite d'avoir la version accolée au nom de l'application genre mynotes-1.0-SNAPSHOT).
  • Les paramètres de connexion à la base de données seront configurables via l'utilisation d'un profile maven : soit développement (par défaut), soit intégration.
pom.xml
Sélectionnez

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.developpez.mynotes</groupId>
    <artifactId>mynotes</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>war</packaging>
    ...
    <name>mynotes</name>
    
    <properties>
        <!-- Database connection parameters for dev -->
        <database.driver>org.hsqldb.jdbcDriver</database.driver>
        <database.url>jdbc:hsqldb:mem:mynotes</database.url>
        <database.username>sa</database.username>
        <database.password></database.password>
    </properties>
    
    <profiles>
        <profile>
            <id>integration</id>
            <activation>
                <property>
                    <name>environment</name>
                    <value>integration</value>
                </property>
            </activation>
            <properties>
            <!-- Database connection parameters for integration -->
                <database.driver>org.hsqldb.jdbcDriver</database.driver>
                <database.url>jdbc:hsqldb:hsql://localhost/mynotes</database.url>
                <database.username>sa</database.username>
                <database.password></database.password>
            </properties>
        </profile>
    </profiles>
    <build>
        <finalName>mynotes</finalName>
        ...
    </build>
    ...
</project>

Afin que le fichier database.properties final puisse être généré par Maven, il devra être placé dans le répertoire src/main/resources-filtered :

database.properties
Sélectionnez

driver=${database.driver}
url=${database.url}
username=${database.username}
password=${database.password}

Dans le profile développement nous obtiendrons :

database.properties
Sélectionnez

driver=org.hsqldb.jdbcDriver
url=jdbc:hsqldb:mem:mynotes
username=sa
password=

Dans le profile integration, nous obtiendrons :

database.properties
Sélectionnez

driver=org.hsqldb.jdbcDriver
url=jdbc:hsqldb:hsql://localhost/mynotes
username=sa
password=

IV-A-2. Ecriture du fichier web.xml

L'application a besoin de fournir un descripteur au container de servlet afin que ce dernier puisse l'installer et la démarrer. Ce descripteur s'exprime sous la forme d'un fichier xml standardisé (web.xml) et selon la norme Maven, il devra être placé dans le répertoire src/main/webapp/WEB-INF.
Dans le fichier web.xml, nous déclarerons notre classe Wicket MyNotesApplication et nous configurerons le contexte Spring :

web.xml
Sélectionnez

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <display-name>mynotes</display-name>
    <servlet>
        <servlet-name>mynotes</servlet-name>
        <servlet-class>org.apache.wicket.protocol.http.WicketServlet</servlet-class>
        <init-param>
            <param-name>applicationClassName</param-name>
            <param-value>com.developpez.mynotes.web.MyNotesApplication</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>mynotes</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>-1</session-timeout>
    </session-config>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:com/developpez/mynotes/dao/applicationContext-dao.xml
            classpath:com/developpez/mynotes/business/applicationContext-business.xml
        </param-value>
    </context-param>
    <context-param>
        <param-name>log4jConfigLocation</param-name>
        <param-value>classpath:log4j.properties</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

Enfin, pour que notre servlet Wicket puisse utiliser Spring, il faut surcharger la méthode init de MyNotesApplication :

MyNotesApplication.java
Sélectionnez

public class MyNotesApplication extends WebApplication {

    ...
    @Override
    protected void init() {
        super.init();
        addComponentInstantiationListener(new SpringComponentInjector(this));
    }
}

Comme nous avons modifié notre code, il faut que nous relancions les tests unitaires de la couche web :

Image non disponible

Nous constatons que le test testRender de HomePageTest ne passe plus. Effectivement, nous instancions MyNotesApplication sans spécifier un contexte Spring. Il y a 2 solutions pour y remédier :

La première est la plus simple et la plus immédiate : il suffit de surcharger la méthode init lors de l'instanciation de MyNotesApplication afin de l'empêcher de charger le contexte Spring (le corps de la méthode sera vide) :

HomePage.java
Sélectionnez

public class HomePageTest {
    ...
    @Before
    public void setUp() throws Exception {
        MyNotesApplication webApplication = new MyNotesApplication() {
            @Override
            protected void init() {
                //surcharge pour ne pas charger le contexte Spring
            }
        };
        tester = new WicketTester(webApplication);
    }
}

L'inconvénient est que cela n'est pas flexible. Si jamais nous devions ajouter des dépendances de services qui seraient exécutés lors du chargement de HomePage, nous serions obligés de préciser un contexte Spring dans notre classe de test.

La deuxième solution consiste à extraire une classe de test abstraite qui serait héritée par HomePageTest et LoggedInPage. Cela permet d'homogénéiser l'écriture et d'éviter la redondance de code. Nous allons partir de la classe LoggedInPageTest puisqu'elle est la plus riche des deux.

  • WicketTester est présent dans les deux classes, cette fixture sera remontée.
  • La méthode setUp sera remontée mais seulement la partie qui initialise le mock du contexte Spring. Ce dernier sera accessible dans les classes concrètes (visibilité protected) si jamais nous devions l'utiliser ou le modifier pour un besoin spécifique.
  • L'initialisation des mocks de service étant spécifique à chaque test, nous écrirons une méthode doSetUp qui sera surchargée dans les classes concrètes de test.

Il en résulte des classes suivantes :

AbstractWicketTestCase
Sélectionnez

public abstract class AbstractWicketTestCase {
    
    protected WicketTester tester;
    
    protected ApplicationContextMock applicationContextMock;


    @Before
    public final void setUp() throws Exception {
        applicationContextMock = new ApplicationContextMock();
        doSetUp();
        MyNotesApplication webApp = new MyNotesApplication() {
            @Override
            public void init() {
                addComponentInstantiationListener(new SpringComponentInjector(this, applicationContextMock));
            }
        };

        tester = new WicketTester(webApp);
    }


    protected abstract void doSetUp() throws Exception;
}
HomePageTest.java
Sélectionnez

public class HomePageTest extends AbstractWicketTestCase {

    @Test
    public void testRender() throws Exception {
        ...
    }


    @Override
    protected void doSetUp() throws Exception {
        SecurityService securityServiceMock = Mockito.mock(SecurityService.class);
        applicationContextMock.putBean("securityService", securityServiceMock);
    }
}
LoggedInPageTest.java
Sélectionnez

public class LoggedInPageTest extends AbstractWicketTestCase {

    //méthodes de tests
    ...

    @Override
    protected void doSetUp() throws Exception {
        SecurityService securityServiceMock = Mockito.mock(SecurityService.class);
        Mockito.stub(securityServiceMock.authenticate("JohnDoe", "seven"))
              .toReturn(UserFactory.buildUser("JohnDoe", "John", "Doe", "johndoe@abc.com", "seven", false));

        applicationContextMock.putBean("securityService", securityServiceMock);
    }
}
Image non disponible

Le point positif de cette écriture est que cela allège la lecture du test. Le point négatif est que l'on doit initialiser nos mocks et les mettre dans le contexte Spring dans chaque classe de test alors que si nous étions passés par des mocks "fait maison", nous aurions simplement écrit une configuration Spring (voir solution 1 du mock dans la couche Service - chapitre 2).

IV-B. Configurer l'environnement d'intégration

L'environnement d'intégration représente l'environnement complet sur lequel les tests fonctionnels seront exécutés. L'application sera donc testée de bout en bout, impliquant l'interaction entre l'action utilisateur sur le client (navigateur dans notre cas), toutes les couches de l'application et la base de données (pas de mock). Nous allons configurer cet environnement en effectuant les taches suivantes :

  • Installation et configuration du serveur de base de données
  • Installation et configuration du container de servlet
  • Pilotage du cycle d'exécution du serveur de Bdd et du container de servlet par Maven

IV-B-1. Installation et configuration du serveur de base de données

Nous allons installer et configurer Hsqldb en mode serveur autonome. Nous mettrons la distribution jar et les fichiers de commandes adéquats dans un répertoire de notre environnement d'intégration (par exemple c:\dev\bdd\hsqldb). Dans notre tutoriel, notre machine de développement fait office de machine d'intégration.

startDBServer.cmd
Sélectionnez

echo "Running HSQLDB server"
java -cp hsqldb.jar org.hsqldb.Server -database.0 file:mynotes -dbname.0 mynotes
stopDBServer.cmd
Sélectionnez

echo "Stopping HSQLDB server"
java -cp hsqldb.jar org.hsqldb.util.SqlTool --sql "shutdown;" localhost-sa

Le script stopDBServer.cmd utilise un identifiant de connexion à la base de données pour pouvoir interagir avec le serveur (localhost-sa). Cet identifiant est défini dans un fichier d'authentification (sqltool.rc) qui sera lu par la classe org.hsqldb.util.SqlTool. Par ailleurs, il devra être déposé dans le répertoire utilisateur (exemple : C:\Documents and Settings\USER):

sqltool.rc
Sélectionnez

# This is for a hsqldb Server running with default settings on your local
urlid localhost-sa
url jdbc:hsqldb:hsql://localhost/mynotes
username sa
password

Ces deux fichiers seront exécutés à la main. Dans un cas standard, la base de données d'intégration est active 24h/24 afin que l'on puisse construire à tout moment l'application (principe de l'Intégration Continue).

IV-B-2. Installation et configuration du container de servlet

Dans l'idéal, le container Jetty devrait être installé sur la machine d'intégration (comme Hsqldb) et les applications web déployées sur celle-ci. Pour les besoins de notre tutoriel, nous allons simplifier la gestion de ce serveur en déléguant le contrôle de son cycle d'exécution par Maven. Jetty démarrera lorsque le profile sera integration et lors de la phase pre-integration-test. Ensuite, il pointera sur le war généré par Maven et l'accès se fera via le port classique 8080. Enfin, l'arrêt du serveur sera effectué lors de la phase post-integration-test.

pom.xml
Sélectionnez

<build>
    <plugins>
        <plugin>
            <groupId>org.mortbay.jetty</groupId>
            <artifactId>maven-jetty-plugin</artifactId>
            <version>6.1.11</version>
            <configuration>
                <webApp>${basedir}/target/mynotes.war</webApp>
                <contextPath>/mynotes</contextPath>
                <port>8080</port>
                <stopKey>arret</stopKey>
                <stopPort>9090</stopPort>
            </configuration>
            <executions>
                <execution>
                    <id>start-jetty</id>
                    <phase>pre-integration-test</phase>
                    <goals>
                        <goal>run-war</goal>
                    </goals>
                    <configuration>
                        <scanIntervalSeconds>0</scanIntervalSeconds>
                        <daemon>true</daemon>
                    </configuration>
                </execution>
                <execution>
                    <id>stop-jetty</id>
                    <phase>post-integration-test</phase>
                    <goals>
                        <goal>stop</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Dans la configuration du plugin jetty, nous avons mis les balises <stopKey> et <stopPort>. Ces deux balises sont obligatoires pour que le plugin puisse lancer le goal stop lors de la phase post-integration-test

IV-B-3. Initialisation du schéma de la base de données

Cette opération s'effectuera à l'aide du plugin maven-antrun-plugin. Nous allons configurer la tache <java/> pour qu'il exécute la classe org.hsqldb.util.SqlTool avec comme argument l'identifiant de la base (cf fichier d'authentification mynotes.rc) et le fichier d'initialisation du schéma mynotes.ddl. Le déclenchement sera effectué lors de la phase compile afin que les tests unitaires de DAO puissent être exécutés sur la base de données d'intégration.

pom.xml
Sélectionnez

<build>
    <plugins>
        ...
        <plugin>
            <artifactId>maven-antrun-plugin</artifactId>
            <executions>
                <execution>
                    <id>init-db</id>
                    <phase>compile</phase>
                    <configuration>
                        <tasks>
                            <java classname="org.hsqldb.util.SqlTool" classpathref="maven.runtime.classpath">
                                <arg value="localhost-sa"/>
                                <arg value="${project.build.directory}/test-classes/mynotes.ddl"/>
                            </java>
                        </tasks>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Le démarrage et l'arrêt du serveur de la base de données ont été omis du cycle pour des raisons technique et pratique.

  • Technique car le lancement de hsqldb doit se faire dans un processus séparé (au sens système). S'il faisait parti d'une phase Maven, il serait tué lors de l'exécution de la phase suivante ce qui serait plutôt gênant pour l'exécution des tests.
  • Pratique car en plus de la remarque évoquée à la fin de la section II-A, ce serveur de base de données peut très bien accueillir d'autres projets. De plus, arrêter puis couper le serveur après chaque build serait très coûteux en temps surtout si l'on décidait de passer à MySql, Oracle ou autre SGBD.

IV-C. Ecriture et exécution du premier test fonctionnel

La phase d'écriture du test fonctionnel (en Anglais functional test ou release test) est cruciale pour valider le besoin à implémenter. Il devra couvrir l'ensemble des cas (séquençage des actions, contrôle des règles de gestion) décrits dans la spécification fonctionnelle. Selon le principe de la méthodologie XP, les tests fonctionnels doivent représenter la(les) spécification(s) fonctionnelle(s) du projet et c'est au client (MOA) de l'écrire avec le binôme de développeurs.

IV-C-1. Introduction de Canoo Webtest

La librairie WebTest permet d'écrire des tests web automatisés dans lesquels des actions utilisateur seront décrites. Ils peuvent être écrits soit en utilisant soit une syntaxe XML, soit Groovy. Par exemple, l'utilisateur veut aller sur la page principale de Google. Il écrira le code suivant :

webtest avec la syntaxe XML
Sélectionnez

<invoke url="http://www.google.com" description="Aller sur la page de Google"/>
<verifyTitle text="Google" description="Verification du titre de la page de google"/>

ou bien

webtest avec la syntaxe Groovy
Sélectionnez

invoke "http://www.google.com", description: "Aller sur la page de Google"
verifyTitle "Google" description: "Verification du titre de la page de google"

Nous voyons que la syntaxe tente de se rapprocher du langage naturel, c'est pourquoi cette plateforme de test semble assez bien convenir pour l'écriture et la lecture de test par une personne non développeur.

Concernant le mode opératoire, Webtest se basant sur Ant, l'exécution s'effectue en arrière plan (pas de navigateur affiché) ce qui permet de gagner énormément de temps sur l'exécution d'une suite de tests. A chaque nouveau rendu de page, une capture d'écran est créée et le résultat du test est décrit sous la forme d'un fichier XML qui pourra ultérieurement être interprété pour générer des états.

Quant à l'intégration de ce framework dans le projet, il peut être intégré dans le projet soit de manière externe via une distribution complète, soit par un artifact Maven 2.

IV-C-2. Test de connexion utilisateur

Nous allons créer un package webtest/usecase sous le répertoire test/resources dans lequel nous écrirons notre premier test. Il contiendra 3 étapes : insertion des données de test, séquence d'actions de l'utilisateur et assertion des données de la base de données à la fin.
La syntaxe utilisée sera la version XML et nous décrirons les étapes suivantes :

  1. Accès à l'application via l'url http://localhost:8080/mynotes.
  2. Saisie des paramètres de connexion et validation du formulaire
  3. Asserter que le message d'accueil est affiché.

La balise verifyText possède une limitation. Lorsqu'elle asserte le texte, les balises html sont également prise en compte alors qu'idéalement elle devrait asserter ce qui est affiché sur la page.

 
Sélectionnez

<verifyText text="Bonjour John Doe et bienvenue dans l'application MyNotes."/>
<!-- Ne marche pas car Canoo trouve "Bonjour <span wicket:id="user">John Doe</span> et bienvenue dans l'application MyNotes." -->

Du coup nous sommes obligés de scinder l'assertion en 3.

UtilisateurNormalTest.xml
Sélectionnez

<?xml version="1.0"?>
<project default="test">
    <target name="test">
        <webtest name="Test connexion into MyNotes">
            <group description="Login">
                <invoke url="http://localhost:8080/mynotes" description="Go to MyNotes"/>
                <setInputField name="userId" value="JohnDoe"/>
                <setInputField name="password" value="seven"/>
                <clickButton label="Connexion"/>
            </group>
            <group description="Verify welcome text">
                <verifyText text="Bonjour"/>
                <verifyText text="John Doe"/>
                <verifyText text="et bienvenue dans l'application MyNotes."/>
            </group>
        </webtest>
    </target>
</project>

Nous allons ajouter l'étape d'insertion des données en base en utilisant la tâche Ant de DBUnit.

UtilisateurNormalTest.xml
Sélectionnez

<?xml version="1.0"?>
<project default="test">

    <target name="test">
        <dbunit driver="org.hsqldb.jdbcDriver" url="jdbc:hsqldb:hsql://localhost/mynotes"
                userid="sa" password=""
                datatypeFactory="org.dbunit.ext.hsqldb.HsqldbDataTypeFactory">
            <operation type="CLEAN_INSERT" src="usecase/UtilisateurNormalInputDataTest.xml" format="xml"/>
        </dbunit>
        <webtest ...>
</project>

Le fichier UtilisateurNormalInputDataTest.xml sera similaire à celui du test unitaire de SecurityDao. Il nous reste l'assertion des données de la base de données après la séquence des actions de l'utilisateur.

UtilisateurNormalTest.xml
Sélectionnez

<?xml version="1.0"?>
<project default="test">

    <target name="test">
        <dbunit ...>
        <webtest ...>
        <dbunit driver="org.hsqldb.jdbcDriver" url="jdbc:hsqldb:hsql://localhost/mynotes"
                userid="sa" password=""
                datatypeFactory="org.dbunit.ext.hsqldb.HsqldbDataTypeFactory">
            <compare src="usecase/UtilisateurNormalOutputDataTest.xml" format="xml"/>
        </dbunit>
</project>

Par rapport aux actions de l'utilisateur, les données de la base ne doivent pas changer. Le fichier DBUnit sera identique à UtilisateurNormalInputDataTest.xml. Cela peut sembler superflu au premier abord mais il aura l'occasion d'évoluer dans les prochains scénarii. Notre premier test étant terminé, il ne reste plus qu'à configurer Maven pour l'exécuter.

IV-C-3. Pilotage de l'exécution de tests fonctionnels par Maven

Nous rappelons les étapes suivantes qui seront exécutées par Maven lors du lancement de la construction du build avec le profile integration :

  1. Initialisation du schéma de la base de données au préalable
  2. Construction du war incluant l'exécution des tests unitaires
  3. Lancement du container de servlet Jetty qui démarrera l'application
  4. Exécution des tests fonctionnels
  5. Arrêt du container de servlet Jetty

Comme Canoo se base sur Ant, nous allons réutiliser le plugin maven-antrun-plugin pour lancer notre test lors de la phase integration-test. La dépendance vers Canoo sera ajoutée dans la configuration du plugin. Il faut noter que la distribution Canoo Maven n'est pas présente dans les dépôts publics Maven. Comme pour iBatis, il faut la télécharger depuis le site officiel et l'installer dans le dépôt local (Attention au numéro de version de l'artifact).

pom.xml
Sélectionnez

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        ...
        <execution>
            <id>run-webtest</id>
            <phase>integration-test</phase>
            <configuration/>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>com.canoo.webtest</groupId>
            <artifactId>webtest</artifactId>
            <version>R_1689</version>
            <exclusions>
                <exclusion>
                    <groupId>javax.xml</groupId>
                    <artifactId>jsr173</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
</plugin>

Le module jsr173 a été exclu de la dépendance car il est obsolète depuis qu'il a été intégré dans la plateforme JDK5.

Ensuite, nous allons définir une propriété Ant runtime_classpath qui pointera vers le classpath de Maven en scope runtime. Enfin nous invoquerons la balise <ant> afin d'exécuter le fichier de lancement des tests allTests.xml qui contiendra notre test. Les attributs inheritAll et inheritRefs de <ant> permettront d'indiquer à Ant que tous les sous-fichiers Ant appelé par allTests.xml accèderont aux propriétés définies dans la configuration.

pom.xml
Sélectionnez

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        ...
        <execution>
            <id>run-webtest</id>
            <phase>integration-test</phase>
            <configuration>
                <property name="runtime_classpath" refid="maven.runtime.classpath"/>
                <property name="webtest.tests"
                         value="${project.build.directory}/test-classes/webtest"/>
                                        
                <ant antfile="${webtest.tests}/allTests.xml" inheritRefs="true"
                                             dir="${webtest.tests}" inheritAll="true"/>
                
            </configuration>
            ...

Il nous reste à écrire le fichier allTests.xml. Par défaut, Canoo ne reconnaît pas les balises Ant de DBUnit, aussi, il faut déclarer la tâche dans notre fichier (d'où l'utilité de notre propriété runtime_classpath). Par ailleurs pour ceux qui ont des problèmes avec les proxys installés sur leur réseau (entreprise ou particulier), il faut créer une petite configuration avec SetProxy. Nous pouvons également surcharger certaines propriétés de Canoo. Dans notre cas, nous voulons que le build stoppe dès la première erreur d'exécution (Exception levée par exemple) et dès le premier test en échec.

allTests.xml
Sélectionnez

<project default="runTests">

    <taskdef resource="webtest_base_relaxed.taskdef">
        <classpath path="${runtime_classpath}"/>
    </taskdef>
    
    <taskdef name="setproxy" classname="org.apache.tools.ant.taskdefs.optional.net.SetProxy"
             classpath="${runtime_classpath}"/>
    <taskdef name="dbunit" classname="org.dbunit.ant.DbUnitTask" classpath="${runtime_classpath}"/>
    
    <setproxy nonproxyhosts="localhost" proxyhost="myproxy" proxyport="80"/>
    
    <property name="wt.config.haltonerror" value="true"/>
    <property name="wt.config.haltonfailure" value="true"/>
    <property name="wt.config.resultpath" value="../webtest-results"/>

    <target name="runTests" description="runs all the tests">
        <ant antfile="usecase/UtilisateurNormalTest.xml"/>
    </target>
</project>

Exécutons notre build avec la commande mvn clean install -Denvironment=integration (ne pas oublier de lancer le script startDBServer.cmd avant)

Image non disponible

Nous pouvons voir que webtest a bien exécuté notre test fonctionnel.

Image non disponible

Canoo "capture" les pages HTML lorsqu'une nouvelle page est rendue. Aussi, en jetant un coup d'oeil, nous nous rendons compte, que d'une part c'est atrocement moche, et que d'autre part il y des soucis d'encodage de caractères (Le lien Déconnexion est mal affiché). Nous y reviendrons dans le scénario suivant.

Vérifions que notre build échoue bien en modifiant notre test :

UtilisateurNormalTest.xml
Sélectionnez

...
<group description="Verify welcome text">
    <verifyText text="Bonjour"/>
    <verifyText text="Robert"/>
    <verifyText text="et bienvenue dans l'application MyNotes."/>
</group>
...
Image non disponible

En ouvrant le rapport d'exécution, nous constatons que l'assertion a échoué.

 
Sélectionnez

<failure message="Step[verifyText (2/2)]: Text not found in page. Expected &lt;Robert>">

IV-C-4. Génération du rapport au format html

Dans la distribution de Webtest, la génération de rapport de test au format HTML est proposée afin que l'on puisse mieux exploiter les données. Afin de conserver la simplicité d'installation et configuration de notre projet, nous allons seulement "récupérer" les fichiers utiles à la génération et l'inclure dans notre répertoire webtest/report/resources.

Image non disponible
Ressources récupérées de la distribution de Canoo 2.0.6

Ensuite nous allons récupérer les taches Ant liées à la génération HTML du fichier webtest.xml de la distribution et les adapter à nos besoins. 2 propriétés seront modifiées : wt.config.resultpath et ~wt.WebTest.dir. Elles pointeront vers des variables définies dans notre pom.xml. Par ailleurs il faudra définir la tâche groovy utilisée pour générer le fichier récapitulant les résultats des tests (En sortant ces cibles Ant de leur contexte d'exécution d'origine, certaines tâches ne seront pas reconnues par défaut lors du lancement de la génération du rapport).

generateReport.xml
Sélectionnez

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="wt.webtest" default="wt.htmlReports">

    <target name="wt.init" description="Initialisation work">

        <property name="wt.config.resultpath" value="${webtest.results}"/>

        <dirname property="~wt.WebTest.dir" file="${webtest.report}/generateReport.xml"
                 description="the directory of the WebTest distribution used"/>

        <taskdef name="groovyScript" classname="org.codehaus.groovy.ant.Groovy"
                 loaderref="wt.defineTasks.loader"
                 classpath="${runtime_classpath}"/>
        ...
    </target>
    
    <target name="wt.htmlReports.init" ...>
    
    <target name="wt.htmlReports" depends="wt.init, wt.htmlReports.init" ...>
</project

Il ne reste plus qu'à définir les valeurs de webtest.results et webtest.report ainsi que l'exécution de generateReport.xml dans notre pom.

 
Sélectionnez

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        ...
        <execution>
            <id>run-webtest</id>
            <phase>integration-test</phase>
            <configuration>
                <tasks>
                    ...
                    <property name="webtest.tests"
                              value="${project.build.directory}/test-classes/webtest"/>
                    <property name="webtest.report"
                              value="${webtest.tests}/report"/>
                    <property name="webtest.results"
                              value="${project.build.directory}/test-classes/webtest-results"/>

                    <ant antfile="${webtest.tests}/allTests.xml" inheritRefs="true"
                         dir="${webtest.tests}" inheritAll="true"/>

                    <ant antfile="${webtest.tests}/report/generateReport.xml"
                         inheritRefs="true"
                         dir="${webtest.tests}" inheritAll="true"/>
                    ...

Ré-exécutons notre commande maven :

Image non disponible

Nous observons que la cible Ant wt.htmlReport de génération a été exécutée. Il ne reste plus qu'à ouvrir la page index.html sous mynotes\target\test-classes\webtest-results.

Image non disponible

IV-C-5. Remaniement du test fonctionnel

Afin d'améliorer la lisibilité du test et de préparer le terrain pour les prochains tests, nous allons remanier quelques éléments. Nous allons créer un répertoire includes dans lequel nous allons déclarer nos tâches DBUnit d'insertion et d'assertion de données. Les noms de fichiers seront des variables positionnées dans le test et les paramètres de connexion à la base de données seront remontés dans le pom.xml.

pom.xml
Sélectionnez

 <plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <id>run-webtest</id>
            <phase>integration-test</phase>
            <configuration>
                <tasks>
                    <property name="runtime_classpath" refid="maven.runtime.classpath"/>
                    <property name="sql.jdbcdriver" value="${database.driver}"/>
                    <property name="sql.url" value="${database.url}"/>
                    <property name="sql.username" value="${database.username}"/>
                    <property name="sql.password" value="${database.password}"/>
                    <property name="sql.datatypefactory"
                              value="org.dbunit.ext.hsqldb.HsqldbDataTypeFactory"/>
                    ...
AssertData.xml
Sélectionnez

<dbunit driver="${sql.jdbcdriver}" url="${sql.url}"
        userid="${sql.username}" password="${sql.password}"
        datatypeFactory="${sql.datatypefactory}">
    <compare src="${outputDataFile}" format="xml"/>
</dbunit>
InsertData.xml
Sélectionnez

<dbunit driver="${sql.jdbcdriver}" url="${sql.url}"
        userid="${sql.username}" password="${sql.password}"
        datatypeFactory="${sql.datatypefactory}">
    <operation type="CLEAN_INSERT" src="${inputDataFile}" format="xml"/>
</dbunit>

Dans le fichier de test UtilisateurNormalTest.xml, nous déclarerons en entête les fragments AssertData et InsertData. Puis nous préciserons les valeurs des propriétés outputDataFile et inputDataFile.

UtilisateurNormalTest.xml
Sélectionnez

<?xml version="1.0"?>

<!DOCTYPE project [
      <!ENTITY insertData__xml SYSTEM "../includes/InsertData.xml">
      <!ENTITY assertData__xml SYSTEM "../includes/AssertData.xml">
]>

<project default="test">

    <property name="inputDataFile" value="usecase/UtilisateurNormalInputDataTest.xml"/>
    <property name="outputDataFile" value="usecase/UtilisateurNormalOutputDataTest.xml"/>

    <target name="test">
        &insertData__xml;

        <webtest name="Test connexion into MyNotes">
            ...
        </webtest>

        &assertData__xml;
    </target>
</project>

Ensuite nous créerons un répertoire config dans lequel nous pourrons déclarer les balises non reconnues par défaut par Ant et modifier certaines propriétés générales de Canoo.

Config.xml
Sélectionnez

<taskdef resource="webtest_base_relaxed.taskdef">
    <classpath path="${runtime_classpath}"/>
</taskdef>

<taskdef name="setproxy" classname="org.apache.tools.ant.taskdefs.optional.net.SetProxy"
         classpath="${runtime_classpath}"/>
<taskdef name="dbunit" classname="org.dbunit.ant.DbUnitTask" classpath="${runtime_classpath}"/>

<setproxy nonproxyhosts="localhost" proxyhost="ehttp1" proxyport="80"/>

<property name="wt.config.haltonerror" value="true"/>
<property name="wt.config.haltonfailure" value="true"/>
<property name="wt.config.resultpath" value="../webtest-results"/>

Nous déclarerons Config.xml dans le fichier allTests.xml.

allTests.xml
Sélectionnez

<?xml version="1.0"?>

<!DOCTYPE project [
      <!ENTITY config__xml SYSTEM "config/Config.xml">
      ]>

<project default="runTests">

    &config__xml;

    <target name="runTests" description="runs all the tests">
        <ant antfile="usecase/UtilisateurNormalTest.xml"/>
    </target>
</project>

Il en resulte alors la structure arborescente suivante :

Image non disponible

IV-C-6. Remarque sur l'exécution d'une suite de tests fonctionnels

Pour l'instant nous n'avons qu'un test fonctionnel. Lors de l'exécution des scénarii suivants, nous serons amenés à en écrire d'avantage. L'inconvénient actuel de Canoo est que la configuration de l'exécution d'une suite de test est limitée. Soit il s'arrête au premier test qui échoue, soit il exécute tous les tests mais le build sera marqué SUCCESSFULL même si certains tests ont échoué. L'idéal serait que la suite de tests s'exécute à la manière de JUnit : Tous les tests sont exécutés, si un a échoué alors un message d'erreur (Exception) est envoyé. Nous aborderons cet aspect à la fin du chapitre suivant et nous verrons qu'un petit développement externe sera de rigueur si l'on veut obtenir ce que nous souhaitons.

IV-D. Conclusion

Nous venons de voir que préparer notre application à subir les tests fonctionnels a nécessité un certain effort. Cet effort payera lors de l'exécution des scénarii suivants car nous verrons que le temps accordé au développement pur (implémentation du besoin) par rapport aux autres tâches augmentera de manière considérable et rendra le développeur plus productif. Notre application pourrait à présent être construite via un outil d'intégration continue (Hudson pour ne pas nommer).

IV-E. Téléchargement

Les sources du projet de ce chapitre sont disponibles via ftpftp ou bien via httphttp. Les librairies externes ont été regroupées dans une archive téléchargeable via ftpftp ou bien via httphttp.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2009 David Boissier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.