TestNG, nouveau framework de tests unitaires Java

Les outils à notre disposition

Afin de décrire les tests, nous utilisons des annotations Java que nous plaçons sur les classes, méthodes de test. Elles permettent de spécifier l'organisation des tests, les configurations à adopter... Par ailleurs, nous disposons des assertions pour l'écriture des tests à proprement dit.
Les annotations utilisées par TestNG se trouvent dans le package org.testng.annotations du framework.
Un listing exhaustif des attributs de chaque annotations est disponibles à l'adresse suivante : http://testng.org/doc/documentation-main.html#annotations

Les annotations de base

Nous allons dans un premier temps décrire les annotations primordiales permettant de décrire nos premiers tests de manière simple, sans utiliser de fonctionnalités avancées. Ces mêmes annotations pourront par ailleurs permettrent d'utiliser certaines fonctionnalités telles que les dépendances entre tests, le nombre d'exécutions d'un test...

Les annotations étudiées sont :

L'annotation @Configuration

Cette annotation est une des deux annotations primordiales du framework TestNG. Elle permet de définir tout ce qui se passe autour des méthodes de tests à proprement parler. Elle permet, entre autres, de spécifier qu'une méthode sera exécutée avant ou après chaque suite de tests, classe de tests ou chaque méthode de test.
Elle permet donc de définir les méthodes correspondantes aux méthodes obligatoires setUp() et tearDown() des tests JUnit. Elle offre toutefois plus de flexibilité quant aux configurations.
Son utilisation est simple et se fait grâce à la demande des attributs de l'annotation.

Nous verrons, par ailleurs, qu'aucune norme de nommage n'est présente et que les noms des méthodes sont spécifiés par les développeurs. Cela permet entre autre d'avoir plusieurs méthodes setUp() et tearDown() qui seront activées selon les groupes de tests par exemple.

Nous allons maintenant passer à quelques exemples de code représentatifs de son utilisation.

Nous pouvons donc bien voir que cette annotation est nécessaire pour définir tout ce qui est périphérique aux tests unitaires.

L'annotation @Test

Cette annotation permet de spécifier qu'une classe ou une méthode correspond à un test (pour une méthode) ou un regroupement de tests (pour une classe).
Nous pouvons par ailleurs avoir plusieurs attributs pour cette annotation notamment les groupes auxquels la méthode de test appartient et sa description.


/**
 * Test faisant parti du groupe "ir"
 */
@Test(groups = "ir", description = "test of group IR")
public void descriptedTestMemberOfGroupIR(){
   /* Code du test */
}

D'autres attributs permettent de spécifier de nombreux comportement tels que :

Nous avons vu les différentes possibilités offertes par les annotations de base du framework TestNG. Nous verrons leurs utilisations dans les exemples disponibles dans une autre rubrique.

Nous pouvons désormais passer à la description des annotations avancées proposées par le framework TestNG.

Les annotations avancées

Les autres annotations disponibles permettent de définir les fonctionnalités cîtées précédemment telles que les Exception, les paramètres... Nous allons définir chacune d'elles afin d'avoir une vision d'ensemble pour les exemples à venir.

L'annotation @Parameters

Cette annotation permet de préciser qu'une méthode de test prendra des paramètres. Cela est valable pour des paramètres spécifiés dans le fichier testng.xml. Les paramètres récupérés dynamiquement grâce à un DataProvider ne sont pas spécifiés grâce à cette annotation. L'annotation prend comme attribut value la liste des variables utilisées pour remplir les paramètres de la méthode de test.


/**
 * Passage de paramètres via le fichier XML pour des infos générales (Base de données)
 * @param test la chaine à tester
 */
@Parameters({ "testP" })
public void paramtest(String test) {
    /* Test utilisant le paramètre */
}

Ici le paramètre sera récupéré dans le fichier testng.xml.


<suite name="Test"  verbose="5" >
  <test name="ParametersTest">
  	<parameter name="testP"  value="une String à tester"/>
  </test>
  <!-- Les classes utilisant les paramètres -->
</suite>

L'annotation @DataProvider

Cette annotation est particulière. Elle annote une méthode qui fournira des données (via un passage en paramètres) aux méthodes de tests qui en ont besoin.
Elle est reconnu dans la suite de tests par son nom. C'est le seul attribut de cette annotation.
La méthode annotée par le @DataProvider doit obligatoirement retourner Object[][] ou Iterator<Object[]>.
Cette annotation est utile dans le cas de paramètres très nombreux, dynamiques et de types complexes (objets métiers).


/**
 * Permet de servir de source de données
 * @return l iterateur des objets créés
 */
 @DataProvider(name = "listAllApprentices")
 public Iterator<Object[]> giveData(){
    /* Récupération d'une liste d'Object[] avec un Iterator */
    /* Les objets récupérés sont des Apprentice */
 }
 
/**
 * Test les paramètres du DataProvider
 * @param apprentice l'apprenti testé
 */
 @Test(dataProvider = "listAllApprentices")
 public void verifyAllApprentice(Apprentice apprentice){
    /* Code du test */
    /* Utilisation des objets récupérés en paramètres à travers le DataProvider spécifié (ici listAllApprentices) */ 
 }

L'annotation @ExpectedExceptions

Cette annotation permet de spécifier qu'une méthode de test doit retourner une Exception d'un type donné (ou plusieurs Exceptions). Cela peut-être utile lorsque l'on teste les levées d'erreurs d'une classe, d'un module. Ainsi on peut vérifier que le comportement attendu en termes d'exception est bien celui présent.


/**
 * Méthode qui attend une Exception.
 */
 @Test()
 @ExpectedExceptions(NullPointerException.class)
 public void methodeException(){
    /* Test devant lever une Exception de type NullPointerException */
 }

L'annotation @Factory

Cette annotation permet de spécifier qu'une méthode sera une Factory de tests. Elle permettra de créer et exécuter des classes de tests à la volée. Cette méthode retournera des Object que TestNG utilisera comme une classe de tests.
Cette méthode doit obligatoirement retourner Object[].
La factory créera des objets d'une autre classe contenant des tests. Souvent le constructeur de cette classe de tests prendra des paramètres dynamiquement créé dans la méthode annotée par @Factory.


/**
 * Méthode qui créé les instances d'une classe de tests et qui les exécutera.
 */
 @Factory
 public Object[] testFactory(){
    /* Test renvoyant un tableau d'instance d'une classe de tests */
    /* Exemple : tableau rempli de cette manière : tab[i] = new SimpleTestClass(param1, ...); */
    /* et retourné : return tab; */
 }
 
/**
 * Classe effectuant des tests. Elle sera instanciée et exécutée par notre Factory.
 */ 
 public class SimpleTestClass {
 
 	/**
 	* Constructeur de notre classe de tests, prenant des paramètres.
 	*/ 
 	public SimpleTestClass(String param1, ...){
 	   /* Sauvegarde des paramètres reçus... */
 	}
 	
 	/**
 	* Méthode de test qui sera exécutée lorsque l'instance de la classe sera exécutée par la Factory
 	*/ 
 	@Test()
 	public simpleTest(){
 	   /* Effectue un test */
 	}
 }

Le fonctionnement se passe en plusieurs étapes :

  1. Mécanisme de génération dynamique des classes de tests grâce à la méthode annotée par @Factory
    1. Récupération ou génération des paramètres dynamiques si nécessaires (base de données...).
    2. La méthode @Factory créé des instances d'une classe contenant des tests TestNG avec des paramètres éventuellement.
    3. Sauvegarde des instances dans un tableau d'Object.
  2. Mécanisme de lancement des instances
    1. Récupération du tableau d'Object contenant les instances de la classe de tests.
    2. Exécution de l'instance comme une classe TestNG.
  3. Récupération des résultats de toutes les exécutions

Les assertions

Les assertions sont utilisées par les tests unitaires pour vérifier nos certitudes dans le code. C'est très utile pour les tests afin de vérifier les propriétés de nos objets testés par rapport au comportement attendu. Si nos certitudes ne sont pas bonnes alors le test échouera.

Les assertions permettent d'exprimer de façon programmée nos certitudes humaines.
Nous pouvons par exemple, être sûr que certaines méthodes ne renvoient pas de valeur null, et donc nous les utilisons sans plus d'attention aux valeurs de retour. Cette certitude peut-être vérifiée par le programme grâce aux assertions.
Ainsi, si jamais des modifications sont apportées aux méthodes utilisées par un autre programmeur, les assertions permettront éventuellement de lever une erreur, montrant que notre certitude n'est plus vérifiée. Nous pourrons alors prendre des mesures de changement de nos certitudes ou bien de retour arrière sur le code des méthodes. Cela permet d'éviter, entre autres, des regressions.

Pour plus d'informations sur les assertions en java, vous pouvez consulter le lien suivant : http://smeric.developpez.com/java/astuces/assertions/

Les assertions dans le framework TestNG

Le framework offre deux jeux d'assertions. Il permet d'utiliser les assertions provenant de JUnit mais aussi un jeu d'assertions qui lui est propre.
Ce dernier permet de garder un ordre identique dans les paramètres des assertions, à savoir : assertXXX( actualValue, expectedValue [, message] ).