TestNG, un autre framework de tests unitaires Java

Sébastien Palud

L'objectif de cet article est de présenter clairement, à travers des exemples simples, le framework de tests unitaires Java TestNG.

- Titre : TestNG, un autre framework de tests unitaires Java

- Auteur : Sébastien Palud

- Parution : 31 août 2007

- Mise à jour : 19 juin 2020

- Licence : Copyright ® 2007 Sébastien Palud. 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.

Sommaire

- I. Présentation

- I-A. Objectifs

- I-B. Prérequis

- II. Le framework TestNG

- II-A. Présentation du framework

- II-B. Fonctionnalités

- II-C. Comparaison avec JUnit 3.8

- II-D. Un point sur JUnit 4.0

- II-D-1. Les nouveautés de JUnit 4.0

- II-D-2. JUnit 4.0 ou TestNG

- II-D-3. Tableau récapitulatif

- III. Les outils à notre disposition

- III-A. Les annotations de base

- III-A-1. L'annotation @Configuration

- III-A-2. L'annotation @Test

- III-B. Les annotations avancées

- III-B-1. L'annotation @Parameters

- III-B-2. L'annotation @DataProvider

- III-B-3. L'annotation @ExpectedExceptions

- III-B-4. L'annotation @Factory

- III-C. Les assertions

- III-C-1. Les assertions dans le framework TestNG

- IV. Lancement d'une classe de tests

- IV-A. Création du fichier testng.xml

- IV-A-1. Les organisations possibles des tests

- IV-A-2. Création par le plugin Eclipse

- IV-B. Lancement des tests

- IV-B-1. Lancement grâce au plugin Eclipse

- IV-B-2. Lancement grâce à une tâche Ant

- V. Exemple simple de tests

- V-A. Principes généraux du test

- V-B. Mise en œuvre des tests : TestNGApprenti.java

- V-B-1. Les méthodes de tests

- V-B-2. Le fichier testng.xml associé

- V-B-3. Exécution des tests

- V-B-4. Les sources de ce test

- VI. Exemple de tests avec passage de paramètres

- VI-A. Principes généraux du test

- VI-B. Mise en œuvre des tests : TestNGParameters.java

- VI-B-1. Les méthodes de tests

- VI-B-2. Exécution des tests

- VI-B-3. Les sources de ce test

- VII. Exemple de généralisation des tests, @Factory

- VII-A. Principes généraux du test

- VII-B. Mise en œuvre des tests : TestNGFactory.java et TestApprentice.java

- VII-B-1. Les méthodes de tests

- VII-B-2. Le fichier testng.xml associé (similaire à celui du passage de paramètres) :

- VII-B-3. Exécution des tests

- VII-B-4. Les sources de ce test

- VIII. Le reporting

- VIII-A. Le reporting de TestNG

- VIII-B. Le plugin JUnitReports

- VIII-C. La dernière possibilité, nos plugins

- IX. Références

- IX-A. Ressources

- IX-A-1. Voici les sites proposant des informations sur le framework TestNG

- IX-B. Webmaster

- IX-C. Les sources du projet

I - Présentation

Ce site a été réalisé par Sébastien Palud dans le cadre des exposés système du cours de Dominique Revuz, maître de conférence à l'Université de Marne La Vallée et directeur de la filière Informatique et Réseaux du dispositif Ingénieurs 2000.

I-A - Objectifs

L'objectif de cet article est de présenter clairement, à travers des exemples simples, le framework de tests unitaires Java TestNG.

Ce framework présente de nombreux avantages notamment le support des annotations de Java 1.5.

I-B - Prérequis

Tout d'abord quelques prérequis pour continuer…

Tests unitaires Java
Afin d'appréhender au mieux le sujet, des notions concernant le fonctionnement général des tests unitaires Java (JUnit) et des assertions sont nécessaires.
Cela vous permettra de comprendre rapidement la philosophie des tests proposés par TestNG.

Annotations, Java
Des connaissances en Java 1.5 sont nécessaires afin de pouvoir suivre les exemples notamment en ce qui concerne les annotations.

II - Le framework TestNG

Le framework, que j'ai choisi de présenter, est jeune et a été développé afin de pallier les manques du framework très connu JUnit (en sa version 3.8).

À l'heure du comparatif des différents frameworks (octobre 2005), JUnit était encore en version 3.8. Lors de la rédaction du site web (mars 2006), la version 4.0 de JUnit a fait son apparition et offre plus de flexibilité et fonctionnalités à JUnit. C'est pourquoi je ferai un point particulier pour parler de JUnit 4.0 comparé à TestNG. Ce point sera moins exhaustif, car non testé et uniquement basé sur les informations du site officiel de JUnit.

II-A - Présentation du framework

TestNG est un framework de tests unitaires Java d'une nouvelle génération. En effet il offre la possibilité aux développeurs d'utiliser une des améliorations importantes de Java 1.5, à savoir les annotations.

C'est par ailleurs un framework très simple à mettre en œuvre et à intégrer dans des projets. En effet, TestNG assure une rétrocompatibilité avec les tests JUnit. Il propose par ailleurs un plugin permettant de l'interfacer dans le l'environnement de développement Eclipse.

Ce framework a retenu mon attention durant un comparatif pour d'autres fonctionnalités évoluées qui en font un utilitaire de tests très puissant. Il offre une flexibilité d'écriture, une organisation très ouverte des tests et des fonctionnalités très avancées comme la généralisation des tests.
C'est ainsi que ce framework a été retenu comme framework de tests unitaires pour le projet de Génie Logiciel de 3e année.

II-B - Fonctionnalités

Afin de clarifier mes propos, voici un rapide aperçu des différentes fonctionnalités offertes par TestNG :

- Organisation des tests très puissante ;

- Absence de contraintes de nommage ;

- Rétrocompatibilité avec les tests JUnit ou transformation des tests JUnit en tests unitaires TestNG ;

- Attente d'Exception sur les tests ;

- Création de dépendances entre les différents tests ;

- Lancement des tests échoués uniquement ;

- Passage de paramètres aux méthodes de tests ;

- Généralisation des tests grâce à des Factory.

II-C - Comparaison avec JUnit 3.8

Pour vous montrer l'intérêt de TestNG, il semble utile de le comparer avec la référence en matière de tests unitaires Java, à savoir JUnit. Nous allons expliquer les différences entre les deux frameworks et nous verrons les intérêts des fonctionnalités citées ci-dessus.

Voici le tableau comparatif des deux frameworks :

Points de comparaison

JUnit 3.8

TestNG 4.4

Intégration à Eclipse

Totale

Totale

Java 1.4

Oui

Oui

Java 1.5

Non

Oui

Flexibilité

Faible

Très bonne

Facilité de mise en œuvre

Très bonne

Très bonne

Organisation des tests

Moyenne

Très bonne

Reporting

Très bon

Très bon

Gestion des Exceptions

Aucune

Oui

Passage de paramètres

Aucun

Oui

Généralisation des tests

Aucune

Oui

Compatibilité JUnit

Oui

Oui

Nous allons maintenant pouvoir expliquer les différents points évoqués dans le comparatif et voir les avantages de TestNG

Intégration à Eclipse

L'intégration dans Eclipse est totale pour chacun des frameworks. Au premier abord, on peut penser que le plugin TestNG ne fournit que peu de fonctionnalités à l'utilisateur. En effet, JUnit propose la création des squelettes des classes de tests. Ce n'est pas le cas du plugin TestNG. C'est compréhensible du fait que le framework TestNG n'impose aucune norme de nommage pour les méthodes de tests. Tout est effectué grâce aux annotations.

Support de Java 1.4 et 1.5

Le framework JUnit 3.8 ne profite d'aucune amélioration apportée par la version 1.5 de Java. Il reste le même que lors de la version 1.4. À l'opposé, TestNG utilise les possibilités de Java 1.5 via les annotations qui permettent de définir les classes de tests. TestNG peut aussi être utilisé en environnement Java 1.4 en utilisant les tags Javadoc, mais ce n'est pas l'utilisation préconisée.

Flexibilité de l'écriture des tests

Le framework TestNG est très souple en ce qui concerne l'écriture des tests. En effet la métaprogrammation fournie par les annotations permet de ne pas devoir suivre de convention de nommage. C'est un avantage indéniable sur JUnit qui impose que les méthodes de tests se nomment test***. L'utilisation des annotations permet, par ailleurs, de donner beaucoup d'informations sur le test comme son groupe d'exécution, ses dépendances…

Facilité de la mise en œuvre

Pour ce qui est de la mise en œuvre, aucun framework ne se détache réellement. En effet, dans les deux cas, nous pouvons très rapidement lancer les tests écrits, et ce, avec une facilité déconcertante.

Organisation des tests

Le framework TestNG continue son avancée dans les fonctionnalités. Il permet de regrouper les tests selon des critères propres au développeur. Pour cela, nous utilisons des groupes attribués aux différents tests. Ainsi, nous pouvons ne lancer que certains tests ou bien exclure certains groupes de l'exécution…
Le framework propose aussi d'imposer un ordre d'exécution. Cela peut permettre de s'assurer que certaines méthodes ont été effectuées avant que d'autres ne se lancent
Nous pouvons par ailleurs spécifier que les tests sont lancés de manière mono ou multithreadée afin d'éventuellement tester la concurrence d'accès à certaines ressources…

Le reporting

Dans les deux cas, nous pouvons avoir un très bon reporting de l'exécution des tests. Cela passe par l'utilisation d'un plugin extérieur tel que JUnitReports. TestNG offre aussi la possibilité d'écrire soit même son reporter.

Gestion des Exceptions Java

TestNG apporte une nouvelle fois de l'innovation dans l'écriture des tests en offrant la possibilité d'obliger un test à attendre une Exception Java. Cela permet de tester que les réactions du programme, à des événements non voulus, provoquent bien les levées d'Exception spécifiées dans le cahier des charges.

Passage de paramètres

JUnit est très limité en ce qui concerne les fonctionnalités avancées, il ne permet pas que les méthodes de tests utilisent des paramètres. TestNG offre cette possibilité et permet d'avoir un passage de paramètre statique, grâce au fichier de configuration testng.xml, ou bien de manière dynamique grâce à l'utilisation de deux annotations.

Généralisation des tests

La généralisation des tests consiste à n'écrire qu'une fois le test. Celui-ci sera ensuite instancié plusieurs fois automatiquement. C'est le comportement nommé Facotry dans les Design-Pattern. Lorsque l'on applique ce Design-Pattern à TestNG, nous obtenons une Factory de tests, permettant l'instanciation multiple et automatique d'un test avec des valeurs différentes pour les tests. Nous verrons que cela est très intéressant lorsque l'on veut tester de nombreuses informations (comme une base de données par exemple).

Compatibilité JUnit

TestNG offre des possibilités pour se rendre compatible avec le géant des tests JUnit. Il permet grâce à une information dans le fichier testng.xml, de spécifier que le test à exécuter est un test JUnit. Il existe par ailleurs, une fonctionnalité permettant de migrer les tests JUnit vers des tests TestNG. Cela permet une rétrocompatibilité totale avec JUnit.

II-D - Un point sur JUnit 4.0

À l'heure de l'écriture du site web, JUnit 4.0 est sorti en version officielle. Cette nouvelle version se voulait utilisatrice des fonctionnalités de Java 1.5.
Finalement qu'en est-il ? TestNG s'est-il fait rattraper par JUnit ? Est-il devenu obsolète à peine arrivé à maturité ?
Ce sont les questions auxquelles je vais essayer de répondre.

II-D-1 - Les nouveautés de JUnit 4.0

Le framework JUnit en sa mouture 4.0 propose en effet la gestion de Java 1.5 avec l'utilisation des annotations comme TestNG.
De ce fait, JUnit n'impose désormais plus de norme de nommage des méthodes de tests, des méthodes de configuration telles que la méthode setUp() ou encore de l'obligation d'étendre de la classe TestCase.

Les annotations disponibles dans JUnit 4.0 sont :

- @Test : permet de spécifier que c'est une méthode de test
Elle permet de spécifier, en attribut, l'attente d'Exception Java, et un timeout pour la méthode de test ;

- @Ignore : permet de spécifier qu'une méthode de test doit être ignorée ;

- @BeforeClass : méthode appelée au lancement d'une classe de tests ;

- @Before : une méthode annotée ainsi sera l'équivalent de la méthode setUp() pour JUnit 3.8 ;

- @AfterClass : méthode appelée à la fin d'une classe de tests ;

- @After : une méthode annotée ainsi sera l'équivalent de la méthode tearDown() pour JUnit 3.8.

II-D-2 - JUnit 4.0 ou TestNG

Finalement nous voyons que JUnit 4.0 apporte peu d'évolution par rapport à JUnit 3.8. Les seules avancées sont l'utilisation des annotations, l'arrivée de la gestion des Exception Java et la possibilité d'ignorer un test.

JUnit 4.0 apporte donc beaucoup moins d'innovations qu'attendu par la communauté. Les annotations apportent la flexibilité qui manquait à l'écriture des tests.
On peut donc affirmer que TestNG reste un framework innovant et très avancé au niveau des fonctionnalités. La gestion des paramètres et la généralisation des tests par l'utilisation du pattern Factory restent des exclusivités. Cela est de bon augure pour l'avenir du framework qui garde une longueur d'avance sur JUnit.
De même le système d'organisation des tests JUnit n'a pas évolué et TestNG reste en avance grâce à l'utilisation complexe de groupes de tests…

On peut en conclure que le choix de TestNG dans un projet reste tout à fait valable dès lors que nous allons utiliser les fonctionnalités exclusives et avancées de TestNG. Pour les autres cas d'utilisation, le choix se fera selon les préférences du programmeur.

II-D-3 - Tableau récapitulatif

Points de comparaison

JUnit 3.8

TestNG 4.4

Intégration à Eclipse

Totale

Totale

Java 1.4

Oui

Oui

Java 1.5

Oui

Oui

Flexibilité

Très bonne

Très bonne

Facilité de mise en œuvre

Très bonne

Très bonne

Organisation des tests

Moyenne

Très bonne

Reporting

Très bon

Très bon

Gestion des Exceptions

Oui

Oui

Passage de paramètres

Aucun

Oui

Généralisation des tests

Aucune

Oui

Compatibilité JUnit

Oui

Oui

III - 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 annotation est disponible à l'adresse suivante : http://testng.org/doc/documentation-main.html#annotations

III-A - 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 permettre 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 :

- @Configuration ;

- @Test.

III-A-1 - 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 autres 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.

- Méthode appelée au lancement d'une classe de tests

/**
* Méthode appelée dès le début de l'exécution de la classe de tests
*/
@Configuration(beforeTestClass = true)
public void setUpData() {
/* Code à effectuer au début de l'exécution */
/* Exemple : ouverture d'une connexion à une base de données */
}

Nous voyons ici que grâce à l'attribut beforeTestClass activé (mis à true), nous pouvons spécifier au framework TestNG que cette méthode devra être exécutée dès le lancement de cette classe de tests.
Nous allons voir par la suite qu'une granularité supérieure permet de spécifier des méthodes appelées avant et après chaque test effectué.

- Méthode appelée à la fin d'une classe de tests

/**
* Méthode appelée à la fin d'une exécution de la classe de tests
*/
@Configuration(afterTestClass = true)
public void tearDownData(){
/* Code à effectuer à la fin de la classe de test */
/* Exemple : fermeture d'une connexion à une base de données */
}

Cette fois-ci, c'est l'attribut afterTestClass qui est actif. Cela permet de configurer une méthode devant être appelée à la fin de l'exécution de la classe de tests.

- Équivalent de la méthode setUp()

/**
* Méthode appelée avant chaque exécution d'un test
*/
@Configuration(beforeTestMethod = true)
public void beforeMethode(){
/* Code à effectuer avant chaque méthode de test */
/* Exemple : préparation des données nécessaires pour le test */
}

Ici c'est l'attribut beforeTestMethod qui est actif. Cette méthode sera appelée avant chaque test. On peut ainsi mettre en place certaines données qui sont nécessaires à l'exécution d'un test.

- Équivalent de la méthode tearDown()

/**
* Méthode appelée après chaque exécution d'un test
*/
@Configuration(afterTestMethod = true)
public void afterMethode(){
/* Code à effectuer après chaque méthode de test */
/* Exemple : remettre le système dans l'état initial */
}

Nous spécifions que l'attribut afterTestMethod est activé. Cette méthode sera appelée après chaque méthode de test exécutée. Elle permet de remettre le système en l'état initial.

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

III-A-2 - 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 partie 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 comportements tels que :

- La dépendance de ce test par rapport à d'autres exécutions de tests ou groupes de tests. Attributs dependsOnMethods et dependsOnGroups.

/**
* Test qui sera appelé une fois que la méthode de test firstTest aura été exécutée
* ainsi que toutes les méthodes de test du groupe init.
*/
@Test(groups = "ir", dependsOnMethods = { "firstTest" }, dependsOnGroups = { "init.*" })
public void dependentTest(){
/* Code du test */
}

Nous spécifions ici les dépendances d'un test à un groupe de tests et une méthode de test particulière. Cela permet d'être sûr d'un ordre d'exécution.

- L'activation d'un test
Le test sera ou non exécuté en fonction de la valeur de cet attribut. Si le test n'est pas activé, il n'apparaîtra pas dans les rapports de tests. C'est une fonctionnalité gênante, car des tests peuvent être perdus.
L'utilisation d'un groupe particulier pour les tests inactifs est préférable.
Attribut enabled.

/**
* Test qui ne sera pas exécuté, car l'attribut enabled est à false
*/
@Test(groups = "ir", enabled = false)
public void testWillNotBeExecuted(){
/* Code du test */
}

- L'obligation de lancer un test
Le test sera lancé malgré qu'il dépende de certains tests qui auront échoué précédemment.
Attribut alwaysRun mis à « true ».

- Le nombre de fois que sera exécuté un test
Attribut invocationCount.

- Le nombre de threads utilisés pour une exécution multiple
Cet attribut est en relation directe avec le nombre d'exécutions du test. Si l'attribut invocationCount n'est pas spécifié, le nombre de thread sera ignoré (car le test ne sera exécuté qu'une fois).
Attribut threadPoolSize.

/**
* Test qui ne sera exécuté 30 fois et dans 3 threads différents.
*/
@Test(groups = "ir", invocationCount = 30, threadPoolSize = 3)
public void testMultiThreadMultipleInvocation(){
/* Code du test */
}

- La spécification d'un timeout pour un test
Le test échouera si jamais le timeout spécifié est dépassé. Cela permet de faire des tests de performances et de montée en charge.
Attribut timeOut.

- Spécification d'une méthode de récupération des paramètres
Cet attribut est très important dans le cadre de la généralisation des tests. Ainsi un test peut être exécuté plusieurs fois avec des paramètres différents, récupérés via une autre méthode particulière.
La valeur de cet attribut représente la méthode où seront récupérés les paramètres nécessaires au test.
Attribut dataProvider.

- Le pourcentage de réussite attendu pour le test
Cet attribut peut-être utile dans le cadre d'exécution multiple d'un test. Cela permet aussi de ne pas attendre d'un test qu'il réussisse tout le temps (par exemple lors d'une montée en charge).
Attribut successPercentage.

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.

III-B - Les annotations avancées

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

III-B-1 - 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>

III-B-2 - 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 reconnue 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 */
}

/**
* Teste 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) */
}

III-B-3 - 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 */
}

III-B-4 - 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éés dans la méthode annotée par @Factory.

/**
* Méthode qui crée 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 :

Mécanisme de génération dynamique des classes de tests grâce à la méthode annotée par @Factory

- Récupération ou génération des paramètres dynamiques si nécessaire (base de données…).

- La méthode @Factory crée des instances d'une classe contenant des tests TestNG avec des paramètres éventuellement.

- Sauvegarde des instances dans un tableau d'Object.

Mécanisme de lancement des instances

- Récupération du tableau d'Object contenant les instances de la classe de tests.

- Exécution de l'instance comme une classe TestNG.

Récupération des résultats de toutes les exécutions

III-C - 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 régressions.

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

III-C-1 - 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] ).

IV - Lancement d'une classe de tests

Dans cette partie, nous allons pouvoir constater que l'organisation des tests est un des points forts du framework TestNG. En effet, les possibilités offertes sont très importantes. Par ailleurs nous verrons les techniques les plus utiles de lancement des tests, à savoir grâce au plugin Eclipse et grâce à une tâche Ant.

IV-A - Création du fichier testng.xml

Le fichier testng.xml est le fichier de configuration du lancement des tests. Il permet de configurer les tests à lancer en fonction de leurs packages, classes, groupes ou encore par groupes de groupes! La DTD de ce fichier .xml permet vraiment énormément de configurations.

Un fichier de configuration s'articule autour de deux éléments que sont la suite qui représente l'ensemble des tests à lancer, et les tests qui représentent chaque test devant être lancé. Un test peut contenir plusieurs classes ou groupes de tests.
Chacun de ces éléments peut avoir un nom. La suite comporte un attribut permettant de spécifier le niveau de log à afficher.

Exemple de fichier :

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="Suite" verbose="5" >
<test name="TestNum1" >
...
</test>
<test name="TestNum2" >
...
</test>
.
.
.
<test name="TestNumN" >
...
</test>
</suite>

Schéma de la DTD du fichier testng.xml

IV-A-1 - Les organisations possibles des tests

Nous pouvons organiser nos lancements de tests selon plusieurs axes :

- Les packages de tests
Nous pouvons demander à ce que tout un package soit inclus au lancement du test. Cela limite les lignes de configuration d'ajout de classes de tests.

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="SuitePackage" verbose="5" >
<test name="FirstTest" >
<packages>
<package name="test.testPkg" />
</packages>
</test>
</suite>

Ici toutes les classes du package test.testPkg comportant des tests seront ajoutées au premier test nommé FirstTest.

- Les classes de tests

Ici nous ajouterons les classes contenant des tests TestNG à notre test.

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="SuiteClass" verbose="5" >
<test name="FirstTest" >
<classes >
<class name="test.testClass.TestSample1" />
<class name="test.testClass.TestSample2" />
</classes >
</test>
</suite>

- Les méthodes de tests d'une classe

Ici nous spécifierons les méthodes de tests que nous voulons lancer sur les classes. Cela permet un affinement des exécutions.

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="SuiteClass" verbose="5" >
<test name="FirstTest">
<classes>
<class name="test.testClass.TestSample1">
<methods>
<include name=".*enabledTestMethod.*"/>
<exclude name=".*brokenTestMethod.*"/>
</methods>
</class>
</classes>
</test>
</suite>

Ici nous voyons que nous avons ajouté les tests des méthodes contenant le mot enabledTestMethod et exclu les méthodes contenant brokenTestMethod.

- Les groupes de tests

C'est sûrement la fonctionnalité la plus intéressante proposée par TestNG. Le lancement des tests en fonction des groupes permet d'avoir des fichiers très précis en ce qui concerne les tests à effectuer.
Nous pouvons regrouper les groupes dans d'autres groupes pour une question de lisibilité. Nous pouvons aussi exclure certains groupes des exécutions. Les tests seront alors notés comme Skipped dans les rapports.
Cette exclusion est utile pour les tests que nous voulons ignorer, en lieu et place de l'attribut enabled, qui ignore le test et l'empêche d'apparaître dans les rapports.

Exemple d'utilisation des groupes avec exclusion

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="SuiteClass" verbose="5" >
<test name="FirstTest">
<groups>
<run>
<include name="groupOk.*"/>
<exclude name="groupNonOk.*"/>
</run>
</groups>

<classes>
<class name="test.testClass.TestSample1"/>
</classes>
</test>
</suite>

Ici nous voyons que nous lancerons les tests du groupe groupOk contenus dans la classe test.testClass.TestSample1 et nous exclurons les tests du groupe groupNonOk.

Exemple de groupes de groupes

Ici nous allons regrouper les groupes et nous demanderons de lancer le regroupement.

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="SuiteClass" verbose="5" >
<test name="FirstTest">
<groups>
<define name="multipleTestGroup">
<include name="firstGroupTest"/>
<include name="secondGroupTest"/>
</define>

<define name="allGroups">
<include name="multipleTestGroup"/>
<include name="singleTestGroup"/>
</define>

<run>
<include name="allGroups"/>
</run>
</groups>

<classes>
<class name="test.testClass.TestSample1"/>
</classes>
</test>
</suite>

Nous avons effectué plusieurs inclusions de groupes dans des groupes de plus haut niveau (multipleTestGroup puis allGroups). C'est ce dernier groupe-ci qui sera lancé à l'appel du test et donc ce sont les groupes contenus qui seront utilisés. Les méthodes lancées seront celles correspondant au groupe allGroups contenues dans la classe test.testClass.TestSample1.

Finalement, énormément de possibilités sont offertes; au programmeur de les utiliser à bon escient.

IV-A-2 - Création par le plugin Eclipse

Le plugin Eclipse permet de créer son fichier testng.xml grâce à une interface graphique offrant globalement les mêmes fonctionnalités.

Aperçu d'une configuration par le plugin

Ainsi nous pouvons ajouter des classes ou des groupes ou encore nous baser sur un fichier déjà créé.

IV-B - Lancement des tests

TestNG permet que les tests soient lancés de plusieurs manières :

- Par le plugin Eclipse ;

- Par une classe Java ;

- En ligne de commandes ;

- Par une tâche Ant.

Nous allons maintenant voir comment lancer les tests grâce au plugin (pour son reporting) et grâce à une tâche Ant (pour l'automatisation des lancements).

IV-B-1 - Lancement grâce au plugin Eclipse

Le plugin Eclipse permet de lancer par une interface graphique les tests définis. C'est la même interface que celle utilisée pour définir la suite de test contenue dans le fichier testng.xml. Une View Eclipse est disponible pour voir les résultats des tests. Elle permet aussi de relancer les tests préalablement échoués.

Lancement d'une suite de test :

Une fois le test exécuté, nous pouvons regarder les résultats dans la vue TestNG.
Nous pouvons voir beaucoup d'informations et effectuer quelques actions :

- Résumé des tests exécutés : nombre de tests lancés, réussis, échoués et ignorés ;

- Résumé de chaque méthode de test échouée : l'erreur Java qui a causé le plantage du test ;

- Relance de tous les tests du fichier de configuration testng.xml ;

- Relance des tests échoués uniquement.

Voici la vue de reporting générale :

Et la vue de reporting d'un test échoué :

IV-B-2 - Lancement grâce à une tâche Ant

Nous venons de voir comment utiliser le plugin Eclipse. Mais ce n'est pas quelque chose d'automatique et encore moins portable.
En effet l'installation du plugin est nécessaire pour bénéficier de ces fonctionnalités. Nous allons voir une méthode plus propre et plus adaptée au déploiement et à des clients des applications.
Je veux bien entendu parler d'une tâche Ant.

La tâche Ant permet d'effectuer plus de choses à l'exécution des tests. En effet, nous pouvons configurer les rapports de tests afin d'avoir des rapports au format HTML notamment.
Cela est très utile pour avoir des traces de nos tests, et ces rapports sont très appréciés dans le cadre d'un projet.

Nous allons donc voir les éléments de base de la tâche Ant. La partie concernant les rapports sera traitée par la suite.

Voici les éléments concernant la configuration de TestNG, comme l'inclusion de la librairie pour Ant :

<!-- Création d'un projet Ant -->
<project name="testng" default="base">

<!-- Spécification de propriétés utilisées par la suite -->
<property name="bin.dir" value="bin/"/>
<property name="lib.dir" value="lib/"/>
<property name="src.dir" value="src/"/>
<property name="src.dir.app" value="src/"/>

<!-- Définition d'un path -->
<path id="class.path">
<fileset dir="${lib.dir}">
<!-- Ajout des jar de TestNG au path pour la suite de l'exécution -->
<include name="*.jar" />
</fileset>
<path location="${bin.dir}"/>>
</path>

...

</project>

Vient ensuite la partie concernant la définition des tâches Ant concernant les exécutions TestNG à proprement parler :

<!-- Création d'une tâche Ant -->
<target name="base">

<!-- Déclaration que c'est une tâche pour TestNG -->
<taskdef name="testng" classname="org.testng.TestNGAntTask" classpathref="class.path"/>

<!-- Utilisation du path défini précédemment -->
<!-- On continue l'exécution malgré un test échoué -->
<testng classpathref="class.path" haltOnfailure="false">

<!-- Spécification de la localisation du fichier testng.xml à exécuter -->
<xmlfileset dir="${src.dir.app}/base" includes="testng.xml"/>

<!-- Spécification d'un argument à la JVM : activation des annotations -->
<jvmarg value="-ea"/>
</testng>
</target>

Nous avons vu comment écrire une tâche pour l'outil commun Ant. L'exécution se fait comme n'importe quelle tâche Ant.

Nous pouvons désormais passer aux différents exemples des fonctionnalités de TestNG décrites plus haut.
Nous voyons ici la vue Eclipse permettant le lancement des différents exemples.

V - Exemple simple de tests

Nous allons maintenant passer à un exemple permettant de mettre en œuvre les principes de base des tests unitaires avec TestNG.

V-A - Principes généraux du test

Dans cet exemple nous allons utiliser deux objets métiers, à savoir :

- Un Apprenti (Apprentice), caractérisé par un nom, un prénom et une filière ;

- Une Filière d'études (Stream), caractérisée par un nom d'école et le libellé de la filière.

Chaque apprenti est donc lié à une filière. Ceci se fait grâce à une référence d'un objet Stream stockée dans l'Apprentice.

Nous allons par la suite stocker des apprentis dans une HashMap afin de pouvoir stocker des éléments et les récupérer par la suite.
Cette HashMap est gérée par une autre classe qui nous permettra de la remplir rapidement, de la vider, de récupérer un élément et d'en récupérer la taille.
Ceci sera utile pour les méthodes de setUp(), tearDown() et pour les assertions.

V-B - Mise en œuvre des tests : TestNGApprenti.java

V-B-1 - Les méthodes de tests

Dans la classe correspondant réellement aux tests, nous allons utiliser des méthodes annotées par @Configuration afin de mettre en place les données nécessaires à nos tests. Nous pouvons citer la création de la HashMap dans la classe ApprenticeData.java, son remplissage et son vidage à la fin de chaque test pour retourner à l'état initial.

Nous avons ensuite 6 méthodes de tests permettant chacune de vérifier les fonctionnements de nos objets :

- managerGetApprenti() pour la vérification du fonctionnement de la HashMap ;

- attributesTestingShouldBeOk() pour tester les valeurs des attributs d'un apprenti récupéré par rapport à l'original ;

- attributesTestingShouldFail() pour vérifier que si on modifie les valeurs alors elles ne seront plus correctes et le test échouera ;

- methodeignore() dont l'attribut enabled de l'annotation @Test est mis à false, pour vérifier que le test ne sera pas exécuté ;

- methodeExceptionOK() pour vérifier le fonctionnement des Exception avec TestNG ;

- methodeExceptionNonOK() qui vérifie que le test échoue si une Exception attendue ne se produit pas.

Exemple de code de méthodes (autres méthodes disponibles dans les sources) :

/**
* Vérifie les attributs de l'apprenti
*/
@Test(groups = "ir", enabled=true)
public void attributesTestingShouldBeOk(){
Apprentice app = _appmap.getApprenti(_appmap.size()-1);

/* Vérification que les attributs sont non nuls */
assertNotNull(app.getName(), "Le nom ne devrait pas etre null");
assertNotNull(app.getFirstName(), "Le prenom ne devrait pas etre null");
assertNotNull(app.getStream(), "La filiere ne devrait pas etre null");

/* Vérification que les valeurs des attributs sont similaires à ceux attendus */
assertEquals(app.getName(), _apprenti.getName(), "Les noms devraient etre identiques");
assertEquals(app.getFirstName(), _apprenti.getFirstName(), "Les prenoms devraient etre identiques");
assertEquals(app.getStream().getLibelle(), _apprenti.getStream().getLibelle(), "Les filieres devraient avoir le meme nom");
assertEquals(app.getStream().getSchoolName(), _apprenti.getStream().getSchoolName(), "Les filieres devraient avoir la meme école d'attache");
}

/**
* Méthode qui attend une Exception.
*/
@Test(groups="ir")
/* On attend une NullPointerException ou une Exception en héritant */
@ExpectedExceptions(NullPointerException.class)
public void methodeExceptionOK(){
throw new NullPointerException();
}

V-B-2 - Le fichier testng.xml associé

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="IR"  verbose="5" >
<test name="Apprenti">
<classes>
<!-- Ajout de la classe de tests définie précédemment -->
<class name="base.TestNGApprenti"/>
</classes>
</test>
</suite>

V-B-3 - Exécution des tests

Nous allons maintenant pouvoir exécuter le test grâce à notre fichier Ant. Nous allons pouvoir voir que les traces Ant nous renseignent énormément sur le déroulement de l'exécution.

# On voit les méthodes de tests trouvées par le moteur de TestNG.
# Nos 6 méthodes sont présentes ainsi que les méthodes de "configuration".
[testng] TESTCLASS: base.TestNGApprenti
[testng] [TestClass] BeforeClass : base.TestNGApprenti.setUpData()
[testng] [TestClass] BeforeMethod:    base.TestNGApprenti.beforeMethode()
[testng] [TestClass] Test        :        base.TestNGApprenti.attributesTestingShouldFail()
[testng] [TestClass] Test        :        base.TestNGApprenti.methodeignore()
[testng] [TestClass] Test        :        base.TestNGApprenti.methodeExceptionNonOK()
[testng] [TestClass] Test        :        base.TestNGApprenti.attributesTestingShouldBeOk()
[testng] [TestClass] Test        :        base.TestNGApprenti.methodeExceptionOK()
[testng] [TestClass] Test        :        base.TestNGApprenti.managerGetApprenti()
[testng] [TestClass] AfterMethod :    base.TestNGApprenti.afterMethode()
[testng] [TestClass] AfterClass  : base.TestNGApprenti.tearDownData()
[testng] [TestClass]
[testng] ======

# Nos méthodes seront exécutées de manière parallèle.
# On voit que la méthode ignorée a disparu de l'exécution.
[testng] [TestRunner] PARALLEL LIST:
[testng] [TestRunner]   base.TestNGApprenti.methodeExceptionNonOK()
[testng] [TestRunner]   base.TestNGApprenti.managerGetApprenti()
[testng] [TestRunner]   base.TestNGApprenti.attributesTestingShouldFail()
[testng] [TestRunner]   base.TestNGApprenti.methodeExceptionOK()
[testng] [TestRunner]   base.TestNGApprenti.attributesTestingShouldBeOk()
[testng] [TestRunner] SEQUENTIAL LIST:
[testng] [TestRunner] ===
[testng] [TestRunner] Found 5 applicable methods

# Nous voyons ici l'ordre d'exécution des méthodes.
# Nous voyons bien les alternances entre tests et méthodes de "configuration".
[testng] [Invoker 5383406] Invoking base.TestNGApprenti.setUpData()
[testng] [Invoker 5383406] Invoking base.TestNGApprenti.beforeMethode()
[testng] [Invoker 5383406] Invoking base.TestNGApprenti.methodeExceptionNonOK
[testng] [Invoker 5383406] Invoking base.TestNGApprenti.afterMethode()
[testng] [Invoker 5383406] Invoking base.TestNGApprenti.beforeMethode()
[testng] [Invoker 5383406] Invoking base.TestNGApprenti.managerGetApprenti
[testng] [Invoker 5383406] Invoking base.TestNGApprenti.afterMethode()

...

[testng] ***********

# Nous avons ici le résumé de l'exécution et donc les résultats.
[testng] PASSED: managerGetApprenti
[testng] PASSED: methodeExceptionOK
[testng] PASSED: attributesTestingShouldBeOk

# Nous avons ici une erreur que nous attendions.
# Nous avons par ailleurs un message donnant la cause de l'erreur.
[testng] FAILED: base.TestNGApprenti.methodeExceptionNonOK()
[testng] org.testng.TestException:
[testng] Expected an exception in test method base.TestNGApprenti.methodeExceptionNonOK()
[testng] at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:556)
[testng] at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:89)
[testng] at org.testng.TestRunner.privateRun(TestRunner.java:622)
[testng] at org.testng.TestRunner.run(TestRunner.java:505)
[testng] at org.testng.SuiteRunner.privateRun(SuiteRunner.java:200)
[testng] at org.testng.SuiteRunner.run(SuiteRunner.java:126)
[testng] at org.testng.TestNG.run(TestNG.java:294)
[testng] at org.testng.TestNG.privateMain(TestNG.java:382)
[testng] at org.testng.TestNG.main(TestNG.java:330)

# Second test devant échouer, avec un message explicite que nous avons spécifié.
[testng] FAILED: base.TestNGApprenti.attributesTestingShouldFail()
[testng] Java.lang.AssertionError: La filiere ne devrait pas etre null expected:<true> but was:<false>
[testng] at org.testng.Assert.fail(Assert.java:73)
[testng] at org.testng.Assert.failNotEquals(Assert.java:345)
[testng] at org.testng.Assert.assertTrue(Assert.java:28)
[testng] at org.testng.Assert.assertNotNull(Assert.java:273)

...

# Résumé de l'exécution du Test Apprenti
# Et création des rapports de tests (demandé dans le fichier testng.xml).
[testng] ===============================================
[testng] Apprenti
[testng] Tests run: 5, Failures: 2, Skips: 0
[testng] ===============================================
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\base\testng-failures.xml
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\base\toc.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\base\Apprenti.properties
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\base\index.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\base\main.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\base\groups.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\base\methods.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\base\classes.html

# Résumé de l'exécution de la suite IR.
[testng] ===============================================
[testng] IR
[testng] Total tests run: 5, Failures: 2, Skips: 0
[testng] ===============================================

Nous avons pu voir le fonctionnement d'un test de bout en bout. Nous voyons que les tests apportent de bonnes informations sur le cœur de nos applications en permettant un debug efficace.

V-B-4 - Les sources de ce test

Les sources du test sont disponibles ici.

VI - Exemple de tests avec passage de paramètres

Dans ce test nous allons pouvoir voir comment fonctionne le passage de paramètres de manière statique, mais aussi de manière dynamique grâce au @DataProvider

VI-A - Principes généraux du test

Dans cet exemple nous utilisons les deux mêmes objets métier que précédemmment, à savoir l'Apprentice et la Stream.

Cette fois les différents objets auront leurs propriétés stockées dans une base de données. J'ai utilisé une base de type MySQL, que j'ai rempli de quatre apprentis et plusieurs filières, dont un apprenti sans filière.

Vous pouvez trouver le fichier concernant de la base Xpose. Cette base sera réutilisée par la suite dans l'exemple de généralisation des tests par une @Factory.

Dans ce test, nous allons utiliser le passage de paramètres statique pour spécifier les paramètres concernant la base de données, à savoir :

- L'URL de la base de données ;

- Le login d'accès à la base ;

- Le password du compte.

Par ailleurs, nous utiliserons un passage de paramètres dynamique (avec un DataProvider) pour effectuer un test sur chaque apprenti de la base. Ce test utilise les données que lui fournit le DataProvider. Le test vérifie que tous les attributs de l'objet Apprentice sont non nuls.

VI-B - Mise en œuvre des tests : TestNGParameters.java

VI-B-1 - Les méthodes de tests

Dans cette classe, nous avons 4 méthodes utilisées par TestNG :

- void setUpData(String url, String user, String password) qui permet d'ouvrir la connexion à la base de données via JDBC ;

- void tearDownData() qui clôt la connexion à la base de données ;

- void verifyAllApprentice(Apprentice apprentice) qui effectuera la vérification des attributs de l'apprenti passé en paramètres.
C'est cette méthode qui spécifiera qu'elle attend des paramètres provenant d'un DataProvider nommé listAll ;

- Iterator<Object[]> giveData() qui sera la méthode exerçant la fonction de DataProvider.
Elle permet de récupérer tous les apprentis de la base ainsi que leur filière.

Passage de paramètres de manière statique
Pour passer des paramètres de manière statique, nous allons utiliser l'annotation @Parameters pour spécifier une attente de paramètres venant du fichier testng.xml.

Voici le code de la méthode setUpData() utilisant cette technique :

/**
* Passage de paramètres via le fichier XML pour des infos générales (Base de données)
* @param url url de la base
* @param user le login de la base
* @param password le password de la base
*/
@Configuration(beforeTestClass = true)
/* On spécifie que l'on attend trois paramètres */
/* et les noms donnés sont les noms des paramètres écrits dans le fichier testng.xml */
@Parameters({ "url", "user", "password" })
public void setUpData(String url, String user, String password) {
try {
Class.forName("com.mysql.jdbc.Driver");
jdbcConnection = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

Le fichier testng.xml associé

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="Parameters"  verbose="5" >
<test name="Parameters">
<!-- Définition des paramètres et de leur valeurs -->
<parameter name="url"  value="jdbc:mysql://localhost:3306/Xpose"/>
<parameter name="user"  value="root"/>
<parameter name="password"  value=""/>
<classes>
<class name="parameters.TestNGParameters"/>
</classes>
</test>
</suite>

Nous venons de spécifier 3 paramètres statiques pour notre méthode citée ci-dessus. Ils sont reconnus par leur nom dans l'annotation @Parameters. Bien entendu, l'ordre des paramètres est important.

Passage de paramètres de manière dynamique
Nous allons maintenant voir comment utiliser les DataProvider afin de récupérer dynamiquement des données, qui seront ensuite utilisées par la méthode de test utilisant ce DataProvider.

Code de la méthode utilisant le DataProvider :

/**
* Test les paramètres du DataProvider
* @param apprentice l'apprenti testé
*/
/* Spécification du DataProvider qui nous donnera les Apprentice attendus en paramètres */
@Test(dataProvider = "listAll", groups = "parameters")
public void verifyAllApprentice(Apprentice apprentice){
/* Vérification des données de l'apprenti reçu en paramètre. */
assertNotNull(apprentice.getName(), "Name should not be null");
assertNotNull(apprentice.getFirstName(), "First name of <"+apprentice.getName()+"> should not be null");
assertNotNull(apprentice.getStream(), "Stream of <"+apprentice.getName()+"> should not be null");
}

Ici nous attendrons que le DataProvider listAll nous fournisse des Apprentice.

Le code de la méthode du DataProvider ne fait que récupérer les informations de la base de données en les plaçant dans une liste. Cette méthode renverra un Iterator sur cette même liste d'Object.

/**
* Permet de servir de source de données
* @return l iterateur des objets créés
*/
/* Définition du DataProvider de nom listAll. */
@DataProvider(name = "listAll")
public Iterator<Object[]> giveData(){

...

return list.iterator();
}

VI-B-2 - Exécution des tests

Grâce à Ant, nous allons exécuter le test et analyser le résultat.

# Une seule méthode de tests a été trouvée.
# La méthode fournissant les données n'est pas comptée.
[testng] TESTCLASS: parameters.TestNGParameters
[testng] [TestClass] BeforeClass : parameters.TestNGParameters.setUpData(java.lang.String, java.lang.String, java.lang.String)
[testng] [TestClass] Test        :        parameters.TestNGParameters.verifyAllApprentice(parameters.Apprentice)
[testng] [TestClass] AfterClass  : parameters.TestNGParameters.tearDownData()
[testng] [TestClass]

...

# Lancement des tests.
# Nous voyons bien que la méthode est appelée 4 fois, ce qui correspond bien au nombre d'apprentis dans la base.
[testng] [TestRunner] Found 1 applicable methods
[testng] [Invoker 5383406] Invoking parameters.TestNGParameters.setUpData(java.lang.String, java.lang.String, java.lang.String)
[testng] [Invoker 5383406] Invoking parameters.TestNGParameters.verifyAllApprentice
[testng] [Invoker 5383406] Invoking parameters.TestNGParameters.verifyAllApprentice
[testng] [Invoker 5383406] Invoking parameters.TestNGParameters.verifyAllApprentice
[testng] [Invoker 5383406] Invoking parameters.TestNGParameters.verifyAllApprentice
[testng] [Invoker 5383406] Invoking parameters.TestNGParameters.tearDownData()
[testng] *********** INVOKED METHODS

# Ici nous voyons les paramètres passés aux méthodes, que ce soit en statique ou dynamique.
# Le password pour la méthode setUpData() est vide d'où le fait que l'on ne le voit pas.
[testng] parameters.TestNGParameters.setUpData(java.lang.String, java.lang.String, java.lang.String)jdbc:mysql://localhost:3306/Xpose root   30971263
[testng] parameters.TestNGParameters.verifyAllApprentice(parameters.Apprentice)parameters.Apprentice@3ee284  30971263
[testng] parameters.TestNGParameters.verifyAllApprentice(parameters.Apprentice)parameters.Apprentice@8965fb  30971263
[testng] parameters.TestNGParameters.verifyAllApprentice(parameters.Apprentice)parameters.Apprentice@867e89  30971263
[testng] parameters.TestNGParameters.verifyAllApprentice(parameters.Apprentice)parameters.Apprentice@1dd7056  30971263
[testng] parameters.TestNGParameters.tearDownData() 30971263

# Résultats des tests
[testng] ***********
[testng] PASSED: verifyAllApprentice
[testng] PASSED: verifyAllApprentice
[testng] PASSED: verifyAllApprentice

# Un des apprentis ne passait pas le test, car il n'a pas de filière.
[testng] FAILED: parameters.TestNGParameters.verifyAllApprentice(parameters.Apprentice)
[testng] java.lang.AssertionError: Stream of <Apprenti2> should not be null expected:<true> but was:<false>
[testng] at org.testng.Assert.fail(Assert.java:73)

...

# Résumé, création des rapports
[testng] ===============================================
[testng] Parameters
[testng] Tests run: 4, Failures: 1, Skips: 0
[testng] ===============================================
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\parameters\testng-failures.xml
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\parameters\toc.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\parameters\Parameters.properties
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\parameters\index.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\parameters\main.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\parameters\groups.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\parameters\methods.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\parameters\classes.html
[testng] ===============================================
[testng] Parameters
[testng] Total tests run: 4, Failures: 1, Skips: 0
[testng] ===============================================

Ce test a permis de montrer comment utiliser les paramètres. C'est très utile, car les tests sont plus dynamiques dès lors que l'on utilise des DataProvider et donc d'automatiser les tests. Cela permet aussi d'utiliser des paramètres de type complexe.

Il faut bien comprendre l'esprit du DataProvider : c'est une méthode particulière de test qui sera exécutée autant de fois que de données fournies.
Il faut bien distinguer ce cas de celui de la Factory où c'est toute une classe qui est appelée autant de fois que de données fournies

VI-B-3 - Les sources de ce test

Les sources du test sont disponibles ici.

VII - Exemple de généralisation des tests, @Factory

Dans cet exemple aussi il faut bien comprendre le fonctionnement de cette Factory. Elle crée des instances d'une classe de tests et les sauvegardes. Lors du lancement, elle les exécutera. La classe exécutée peut, bien entendu, comporter autant de tests que voulu.

VII-A - Principes généraux du test

Cet exemple utilise les mêmes éléments que celui concernant le passage de paramètres. On utilise la même base de données avec les mêmes informations stockées. Nous effectuerons le même test que pour les paramètres, mais en utilisant une classe de test externe au lieu d'une méthode et d'un DataProvider.

Nous allons maintenant voir comment mettre cette technique en pratique.

VII-B - Mise en œuvre des tests : TestNGFactory.java et TestApprentice.java

VII-B-1 - Les méthodes de tests

La Factory : TestNGFactory
Dans la classe correspondant à la Factory, nous avons deux méthodes :

- Object[] createInstances(String url, String user, String password)
Méthode qui sera annotée par @Factory. Elle permettra de récupérer les informations de la base de données et de créer des instances de la classe TestApprentice. Ces instances seront celles exécutées ensuite ;

- tearDownData() qui ferme juste la connexion JDBC à la base de données.

Voici les éléments principaux du code de la méthode annotée par @Factory :

/**
* Définition de la Factory
* On crée un tableau d'instances de la classe de test avec l'objet à tester
* Les invocations de cette classe seront faites automatiquement grâce à la FACTORY
*
* Passage de paramètres de la base
* @param url url de la base
* @param user le login de la base
* @param password le password de la base
* @return un tableau d'instances des tests à lancer
*/
@Factory
@Parameters({ "url", "user", "password" })
public Object[] createInstances(String url, String user, String password){

...

/* Tableau contenant nos instances. */
Object[] results = null;

...

/* Récupération des apprentis */

...

/* Instanciation et ajout dans notre tableau stockant les instances. */
/* Ici on stocke un apprenti avec filière. */
results[i] = new TestApprentice(new Apprentice(tab[0], tab[1], new Stream(tab[2], streamSchool)));
}
else{
/* Instanciation et ajout dans notre tableau stockant les instances. */
/* Ici on stocke un apprenti sans filière. */
results[i] = new TestApprentice(new Apprentice(tab[0], tab[1], null));
}

...

return results;
}

La classe de tests : TestApprentice
Pour la classe que la Factory instancie et qui effectuera réellement les tests, nous avons :

- TestApprentice(Apprentice apprentice)() qui est le constructeur et permet l'instanciation par la Factory de la classe ;

- verifyApprentice() qui effectuera les tests sur l'apprenti récupéré à la construction de la classe.

Code des deux méthodes :

/**
* Constructeur de la classe de test
* @param apprentice
*/
public TestApprentice(Apprentice apprentice){
this.apprentice = apprentice;
}

/**
* Test les paramètres de l'apprenti qui est passé à la création de la classe de test via la Factory
*/
@Test(groups = "factory")
public void verifyApprentice(){
/* Vérification des attributs de l'apprenti en variable de classe. */
assertNotNull(apprentice.getName(), "Name should not be null");
assertNotNull(apprentice.getFirstName(), "First name of <"+apprentice.getName()+"> should not be null");
assertNotNull(apprentice.getStream(), "Stream of <"+apprentice.getName()+"> should not be null");
}

VII-B-2 - Le fichier testng.xml associé (similaire à celui du passage de paramètres) :

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="Factory"  verbose="5" >
<test name="Factory">
<!-- Définition des paramètres et de leur valeur -->
<parameter name="url"  value="jdbc:mysql://localhost:3306/Xpose"/>
<parameter name="user"  value="root"/>
<parameter name="password"  value=""/>
<classes>
<class name="parameters.TestNGFactory"/>
</classes>
</test>
</suite>

VII-B-3 - Exécution des tests

Le lancement s'effectue toujours de la même manière et va nous permettre de voir des éléments particuliers à la Factory. En effet, même si on exécute la Factory, c'est la classe instanciée que l'on retrouvera dans les logs. C'est logique, la Factory n'est présente que pour instancier la classe de test de manière dynamique et de les lancer par la suite.

# On voit ici que l'on exécute la classe de la Factory.
[testng] TESTCLASS: factory.TestNGFactory
[testng] [TestClass]
[testng] ======
[testng] [TestClass]

# Découverte de la classe que la Factory instancie et de sa méthode de test.
[testng] ======
[testng] TESTCLASS: factory.TestApprentice
[testng] [TestClass] Test        :        factory.TestApprentice.verifyApprentice()
[testng] [TestClass]
[testng] ======

# Exécution de cette méthode de test autant de fois qu'instanciée par la Factory (4).
[testng] [Invoker 41771] Invoking factory.TestApprentice.verifyApprentice
[testng] [Invoker 41771] Invoking factory.TestApprentice.verifyApprentice
[testng] [Invoker 41771] Invoking factory.TestApprentice.verifyApprentice
[testng] [Invoker 41771] Invoking factory.TestApprentice.verifyApprentice
[testng] *********** INVOKED METHODS
[testng] factory.TestApprentice.verifyApprentice() 9004539
[testng] factory.TestApprentice.verifyApprentice() 8814217
[testng] factory.TestApprentice.verifyApprentice() 31289430
[testng] factory.TestApprentice.verifyApprentice() 16399041

# Résumé, on voit les 4 lancements de la méthode de la classe de tests.
[testng] ***********
[testng] PASSED: verifyApprentice
[testng] PASSED: verifyApprentice
[testng] PASSED: verifyApprentice

# Erreur, car l'apprenti n'a pas de filière.
[testng] FAILED: factory.TestApprentice.verifyApprentice()
[testng] java.lang.AssertionError: Stream of <Apprenti2> should not be null expected:<true> but was:<false>

...

# Résumé de l'exécution et création des rapports.
[testng] ===============================================
[testng] Factory
[testng] Tests run: 16, Failures: 4, Skips: 0
[testng] ===============================================
[testng] [Invoker 41771] Invoking factory.TestNGFactory.tearDownData()
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\factory\testng-failures.xml
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\factory\toc.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\factory\Factory.properties
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\factory\index.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\factory\main.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\factory\groups.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\factory\methods.html
[testng] Creating E:\workspace\Xpose_TestNG\reports\testng\factory\classes.html
[testng] ===============================================
[testng] Factory
[testng] Total tests run: 4, Failures: 1, Skips: 0
[testng] ===============================================

Nous avons ainsi vu que la Factory permettait d'instancier dynamiquement une classe de tests. Les méthodes réellement exécutées n'ont finalement aucun lien avec la Factory qui ne fait que son travail de création d'instances.

VII-B-4 - Les sources de ce test

Les sources du test sont disponibles ici.

VIII - Le reporting

Le framework TestNG propose encore une fois un système de reporting assez intéressant du fait qu'il est ouvert. En effet,
TestNG propose un reporting de base, mais propose en même temps de pouvoir faire soit même son propre outil de reporting.
Il permet aussi d'utiliser des plugins déjà tout faits tels que JUnitReports dont nous parlerons ici.

VIII-A - Le reporting de TestNG

Le reporting de TestNG est relativement fourni en informations concernant les tests. Il permet notamment de récupérer les résultats classés par groupes, ou encore l'ordre d'exécution des tests… Il permet bien entendu de récupérer les erreurs des tests échoués et d'avoir un récapitulatif du test exécuté.

Afin de demander la création des rapports, il suffit de rajouter un attribut dans la balise <testng>. L'attribut outputDir permet de spécifier où seront stocker les rapports.

Le seul point négatif au reporting de TestNG est son aspect visuel. Il n'est pas très agréable à regarder et semble un peu brouillon.

C'est à ce moment qu'entrent en jeu les systèmes de création de nos propres journaux d'exécution ou encore des plugins externes.

VIII-B - Le plugin JUnitReports

Pour utiliser JUnitReports, peu de configuration 0 effectuer. Il suffit de créer une nouvelle tâche Ant qui exécutera la transformation du fichier de résultats du test en une page web complète, agréable et sobre.

Exemple de tâche Ant pour JUnitReports :

<target name="reports-base" depends="base">
<mkdir dir="reportsJunit/base"/>
<junitreport todir="reportsJunit/base">
<fileset dir="reports/testng/base">
<include name="*.xml"/>
<exclude name="testng-failures.xml"/>
</fileset>
<report format="noframes" todir="reportsJunit/base"/>
</junitreport>
</target>

Nous voyons bien qu'il faut utiliser une balise particulière <junitreport> et spécifier le chemin de sortie. Cette balise contient aussi un chemin vers le fichier xml de sortie de TestNG.

Exemple de rapport JUnitReports :

Nous voyons que le design est vraiment mieux, mais nous ne pouvons pas voir les groupes des tests… C'est sûrement dû au fait que le plugin était fait pour JUnit 3.8.

VIII-C - La dernière possibilité, nos plugins

Il est important de noter que chacun peut définir son propre rapport de sortie en développement ses propres Listener et le Reporter.
Cette phase ayant été très peu documentée, je ne l'ai pas testée. Malgré tout la documentation venant d'arriver sur le site officiel est de bonne qualité et peut-être d'un grand secours si on veut mettre en place notre reporting personnel.

IX - Références

Afin de réaliser cette présentation, j'ai recherché des informations sur le framework TestNG. Voici le récapitulatif de toutes les références utilisées pour la rédaction des documents.

IX-A - Ressources

IX-A-1 - Voici les sites proposant des informations sur le framework TestNG

Le site web officiel du framework TestNG [en] : http://testng.org/

Le blog du développeur de TestNG [en] : http://beust.com/

Un site web fournissant des informations de base sur TestNG [en] : http://jyperion.org/articles/testng/testng.htm

IX-B - Webmaster

Pour toutes questions, suggestions ou problèmes sur ce sujet, contactez-moi par mail à l'adresse suivante : sebastien.palud@wanadoo.fr

IX-C - Les sources du projet

Les sources concernant mon exposé sont disponibles au lien suivant : sources Xposé TestNG.
Vous pourrez ainsi analyser plus finement les différents exemples.

La présentation du sujet est elle aussi disponible ici : présentation TestNG.