Publié par

Il y a 10 ans -

Temps de lecture 10 minutes

Automatiser ses tests fonctionnels avec Ant

Les tests unitaires sont largement répandus et leur utilisation est facilitée grâce à des outils matures tels que JUnit, Unitils, ou les apis de Mocking. Au contraire, la pratique des tests fonctionnels reste encore délicate. Même si des outils comme Selenium, Fitnesse, ou HttpUnit facilitent la création de tests fonctionnels, le problème majeur reste d’automatiser l’exécution des tests, qui sont fortement liés à l’environnement d’exécution de l’application (base de données, serveur, dossiers de travail, fichiers de configuration, etc.). Je vais vous présenter une manière de réaliser l’automatisation, il existe des solutions alternatives utilisant DbUnit et Cargo, dont je vous ferai part.

Ainsi, l’automatisation des tests fonctionnels doit permettre de :

  1. Initialiser la base de données avec un jeu de données.
  2. Construire le WAR de l’application.
  3. Démarrer le serveur web et déployer l’application.
  4. Exécuter les tests fonctionnels.
  5. Arrêter le serveur web.

Pour réaliser cela, cet article s’appuie sur l’outil Ant, qui reste encore répandu malgré Maven. Le passage de l’un à l’autre peut s’effectuer sans avoir à tout re-développer, car j’ai implémenté principalement du code Java réutilisable.
Cet article se concentre sur l’automatisation au niveau de la base de données et du serveur web. Ainsi, la construction du WAR et l’exécution des tests fonctionnels ne seront pas abordés.

La base de données

L’idée est d’automatiser la création d’une base de données vierge et d’y insérer un jeu de données, ceci à chaque lancement des tests. Les tests disposent ainsi de données identiques et cohérentes à l’exécution.

Bien qu’un plugin SQL pour Ant existe, il ne permet pas d’exécuter tous les types de requêtes SQL et ne fonctionne que dans des cas simples.
Il existe aussi l’outil DbUnit, qui est une extension à JUnit utilisable via Ant. Très performant, celui-ci permet d’insérer et de garantir la cohérence des jeux de données entre chaque éxecution de tests. Bien adapté aux tests unitaires, DbUnit reste encore peu pratique à utiliser dans le cadre de tests fonctionnels utilisant des jeux de données énormes et complexes: procédures stockées, fonctions, etc.
Ainsi, j’ai créé mon propre code Java sous forme d’une tache Ant, qui parse n’importe quel fichier SQL et exécute les requêtes via l’api JDBC.

Pour créer une tâche Ant, il suffit simplement de coder une classe Java (exemple: DbExecuteTask) étendant la super-classe org.apache.tools.ant.Task. Ensuite, il faut implémenter le code en surchargeant la méthode execute(). L’initialisation des paramètres d’entrée se fait de manière classique via des setters.

import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;
// ...
public class DbExecuteTask extends Task {

    //...variable declaration

    // Setters declaration
	public void setSqlFile(String sqlFile) {
		this.sqlFile = sqlFile;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public void setUser(String user) {
		this.user = user;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public void setJdbcDriver(String jdbcDriver) {
		this.jdbcDriver = jdbcDriver;
	}

    public void execute() throws BuildException {
        //Parsing of the sql file & Jdbc implementation
    }
}

Je ne rentrerai pas dans le détail de l’implémentation de cette classe, à part que j’ai simplement utilisé l’api JDBC pour exécuter les requêtes.

Pour utiliser cette tâche dans votre fichier Ant, il faut d’abord déclarer celle-ci via l’instruction taskdef:

<taskdef name="dbUpdater" classname="fr.xebia.ant.dbUpdater.DbExecuteTask" classpathref="ant.libs"/>

Classpathref: Il est nécessaire d’indiquer dans le classpath les classes et jars nécessaires à l’exécution de la classe DbExecuteTask (jars d’Ant, jar du driver JDBC, etc.) Pour ma part, j’ai packagé cette classe dans un jar pour pouvoir la réutiliser dans d’autres projets.

Pour exécuter cette tâche, il suffit d’effectuer l’appel suivant:

<dbUpdater 
	sqlFile="/path/to/file"
	jdbcDriver="jdbc.driver"
	url="jdbc.url"
	user="jdbc.user"
	password="jdbc.passwd"	/>

Il est possible d’améliorer cela en utilisant une macro-définition, que j’ai nommée execsql pour éviter d’avoir à indiquer les mêmes paramètres de connexion à chaque appel. On décrit ainsi une macro-définition execsql, qui encapsule l’exécution de la tâche dbUpdater. Elle a pour seul paramètre ‘file’: le path vers le fichier SQL.

<macrodef name="execsql">
	<attribute name="file" />
	<sequential>
		<dbUpdater 
			sqlFile="@{file}"
			jdbcDriver="jdbc.driver"
			url="jdbc.url"
			user="jdbc.user"
			password="jdbc.passwd"	/>
	</sequential>
</macrodef>

Voici la tache Ant finale qui lance l’ensemble des fichiers SQL :

<target name="fr.xebia.test.createdb" depends="some.targets">
	<echo message="Deleting all users objects ..."/>
	<execsql file="path/to/sql/file" />
	<echo message="Creating db schema ..."/>
	<execsql file="path/to/sql/file" />
	<echo message="Creating data sets ..."/> 
	<execsql file="path/to/sql/file" />

</target>

Voici en cadeau, le script SQL qui permet d’effacer tous les objets d’une base de données créés par les utilisateurs. (Attention à ne pas l’exécuter sur la mauvaise base !)

purge recyclebin
/
declare
ddl varchar2(32000);
nl varchar2(1) := chr(10);
begin
    for i in (
    select object_name,object_type from user_objects
    where object_type in ('TABLE','VIEW','FUNCTION','PROCEDURE',
    'PACKAGE','SEQUENCE','JOB','SYNONYM','TYPE')
    )
    loop
        ddl := 'DROP '||i.object_type||' '||i.object_name;
        if i.object_type in ('TABLE','VIEW') then
            ddl := ddl||' CASCADE CONSTRAINTS';
        elsif i.object_type = 'TYPE' then
            ddl := ddl||' FORCE';
                elsif i.object_type = 'JOB' then
                        ddl := 'begin dbms_scheduler.drop_job('''||i.object_name||''',true); end;';
        end if;
        dbms_output.put_line(ddl);
        execute immediate ddl ;
    end loop;
end;
/
purge recyclebin
/

Le serveur web

J’ai choisi d’utiliser le serveur embarqué Jetty qui est très répandu et facile d’utilisation.
Pour installer Jetty, je vous renvoie sur la documentation officielle Rien de compliqué, il suffit juste d’ajouter quelques jars.

Bien qu’un plugin Jetty pour Ant existe et fonctionne très bien, celui-ci ne permet pas d’arrêter le serveur automatiquement. J’aurai pu utilisé le wrapper Cargo qui est compatible avec de nombreux conteneurs et fonctionne avec Ant. A la place, j’ai choisi une autre solution proposé sur le blog suivant.
Pour réaliser cela, il faut implémenter une classe Java JettyStart qui démarre le serveur Jetty, et déploie l’application. L’objet org.mortbay.jetty.Server correspond à l’instance du serveur et est stocké en attribut de classe statique.

import org.mortbay.jetty.Connector;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.bio.SocketConnector;
import org.mortbay.jetty.webapp.WebAppContext;
// ..
public class JettyStart {
	
	     private static Server server;  
	     private static final int JETTY_DEFAULT_PORT = 8090; 
	     private static final int MONITOR_THREAD_DEFAULT_PORT = 8091; 
	     private static final String CONTEXT_PATH = "/xebiasite";
	   
	     public static void main(String[] args) throws Exception { 
	    	 if(args.length < 1){
	    		 System.out.println(" War path is required.");
	    		 return ;
	    	 }
	    	 int jettyPort = JETTY_DEFAULT_PORT;
	    	 if(args.length == 2){
	    		 jettyPort = Integer.parseInt(args[1]);
	    	 }

	         server = new Server();  
	         SocketConnector connector = new SocketConnector();  
	         connector.setPort(jettyPort);  
	         server.setConnectors(new Connector[] { connector });  
	         WebAppContext context = new WebAppContext();  
	         context.setServer(server);  
	         context.setContextPath(CONTEXT_PATH);  
	         context.setWar(args[0]);  
	         server.addHandler(context);  
	         Thread monitor = new MonitorThread();  
	         monitor.start();  
	         server.start();  
	         server.join();  
	     }  

// ...

}

On peut voir que la méthode main de JettyStart reçoit les arguments suivants : chemin vers le WAR et port de Jetty (facultatif) : en seulement quelques lignes de code, on démarre le serveur Jetty.

Pour gérer l’arrêt du serveur, j’ai créé un thread MonitorThread (Classe interne à JettyStart), qui écoute sur un certain port socket. Dès que le thread reçoit une connexion sur ce port, il coupe le serveur Jetty.

// JettyStart class

private static class MonitorThread extends Thread {  
	   
	         private ServerSocket socket;  
	   
	         public MonitorThread() {  
	             setDaemon(true);  
	             setName("StopMonitor");  
	             try {  
	                 socket = new ServerSocket(MONITOR_THREAD_DEFAULT_PORT, 1, InetAddress.getByName("127.0.0.1"));  
	             } catch(Exception e) {  
	                 throw new RuntimeException(e);  
	             }  
	         }  
	   
	         @Override  
	         public void run() {  
	             System.out.println("*** running jetty 'stop' thread");  
	             Socket accept;  
	             try {  
	                 accept = socket.accept();  
	                 BufferedReader reader = new BufferedReader(new InputStreamReader(accept.getInputStream()));  
	                 reader.readLine();  
	                 System.out.println("*** stopping jetty embedded server");  
	                 server.stop();  
	                 accept.close();  
	                 socket.close();  
	             } catch(Exception e) {  
	                 throw new RuntimeException(e);  
	             }  
	         }  
	     }  

Pour arrêter le serveur, il faut exécuter la classe JettyStop qui se connecte sur la socket, pour avertir MonitorThread. Celui-ci, arrête alors le serveur Jetty via l’attribut statique: server.stop();.

public class JettyStop {

	  public static void main(String[] args) throws Exception {  
		 Socket s = new Socket(InetAddress.getByName("127.0.0.1"), 8091);  
		 OutputStream out = s.getOutputStream();  
		 System.out.println("*** sending jetty stop request");  
		 out.write(("rn").getBytes());  
		 out.flush();  
		 s.close();  
         }  
}

Pour exécuter la classe JettyStart depuis Ant, il suffit d’utiliser l’instruction java :

<java classname="fr.xebia.jetty.JettyStart" fork="true">
      <arg value="${path/to/war/file}"/>
      <arg value="8090"/>
      <classpath>
          <path refid="path/to/need/librairies"/>
	  <pathelement path="path/to/testclasses"/>
      </classpath>
</java>

Attention, il faut ajouter dans le classpath les bonnes classes ainsi que les jars nécessaires (ceux pour Jetty en particulier). Pour plus d’infos sur la gestion du classpath avec Jetty, je vous conseille cette page.

Résultat final

Après avoir automatisé la création de la base de données et le démarrage du serveur web, il reste à rassembler l’ensemble dans un target Ant. Bien sûr, avant d’exécuter cette tâche, il faut au préalable construire le WAR et compiler les classes de tests: depends="clean,fr.xebia.package.war,fr.xebia.compile.test".

On lance en premier la tâche de création de la base de données fr.xebia.test.createdb. Puis, on démarre le serveur Jetty en exécutant la classe JettyStart comme on l’a vu plus haut.
Avant de lancer les tests fonctionnels, il est nécessaire d’attendre le déploiement de l’application. Pour réaliser cela, j’ai utilisé l’instruction waitfor qui écoute sur le port du serveur. Tant que le serveur n’est pas démarré, la suite de l’exécution de la tâche est bloquée.
Les tests fonctionnels (Selenium par exemple) sont ensuite exécutés via la tâche fr.xebia.test.selenium. Enfin, on arrête le serveur en exécutant la classe JettyStop.

 <target name="fr.xebia.test.jetty.cycle" depends="clean,fr.xebia.package.war,fr.xebia.compile.test"> 
		 	
	     <antcall target="fr.xebia.test.createdb"/>
	     <parallel>  
		 <java classname="fr.xebia.jetty.JettyStart" fork="true">
		    <arg value="${path/to/war/file}"/>
		    <arg value="8090"/>
		    <classpath>
		    	<path refid="path/to/need/librairies"/>
	    	        <pathelement path="path/to/testclasses"/>
		    </classpath>
		 </java>
	 	 <sequential>
	             <waitfor> 
	                 <socket server="127.0.0.1" port="8090"/>  
	             </waitfor>  
	             <antcall target="fr.xebia.test.selenium"/>
	             <java classname="fr.xebia.jetty.JettyStop" classpath="path/to/testclasses"/>  
	 	 </sequential>
	     </parallel>
 </target> 

Conclusion

Je vous ai présenté une manière d’automatiser l’exécution des tests fonctionnels. Cette solution a été mise en place sur un projet utilisant Ant. D’autres solutions sont possibles en fonction du projet. Je pense notamment à l’utilisation de DbUnit pour la base de données et de Cargo pour contrôler le serveur web.
Au final, on peut constater qu’il est relativement simple d’implémenter ses propres tâches Ant pour réaliser l’automatisation. L’utilisation de Jetty en serveur web embarqué dans du code Java facilite grandement le travail.

Publié par

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.