Introduction
Un des éléments les plus importants dans le développement d’un logiciel est le test des différentes fonctionnalités de ce dernier. Les limites trop serrées de date limite de développement et l’aspect un peu répétitif et peu agréable du développement des tests font en sorte que, malheureusement, le développement de tests est souvent laissé pour compte. Il s’agit néanmoins d’une étape capitale au développement d’un logiciel fiable et robuste.
On a souvent une version très limitée de l’utilisation des tests. Tout programmeur comprendra que les tests doivent:
-
- Valider ce que l’application fait;
- Permets de détecter des « bugs » dans le programme
Par contre, les tests permettent également de:
-
- Documenter les classes
- Indiquer les limites de ces classes
- Prévoir l’écriture des modules du programme
- En effet, le « Test Driven Developpement » consiste à écrire les tests des modules avant que ces modules soient créés.
- L’application ne fait pas ce qu’elle n’est pas censée faire
- En d’autres mots, que l’application fait ce qu’elle est créée pour faire, et rien d’autre
- Documenter les classes
Les différents types de tests
Il existe plusieurs types de tests qui permettent de valider différents aspects du logiciel. Les plus courants sont les tests suivants:
-
- Tests unitaires (parfois appelé « White Box Testing »):
- Permet de tester individuellement les modules de programmation. En programmation orientée objet par classes, les modules de programmation sont les classes en tant que telles.
- Dans les tests unitaires, le test connaît la structure (et non le code précis des méthodes) de la classe et l’utilise afin de valider chacune de ses fonctionnalités.
- Tests de non-régression (parfois appelé « Black Box Testing »):
- Ce type de tests permet de valider l’expérience utilisateur;
- Valide l’interface et les valeurs visibles par l’utilisateur;
- Le test doit prendre en compte ce que le logiciel doit faire, et non la structure des classes;
- Ce type de tests ne nécessite pas l’utilisation de la programmation. Par exemple, un adjoint administratif pourrait effectuer ces tests.
- Tests unitaires (parfois appelé « White Box Testing »):
On appelle Tests système l’ensemble de tous les tests (unitaire et de non-régression) du système.
On appelle une suite de tests l’ensemble des tests s’appliquant à une partie de logiciel. Par exemple, on pourrait avoir une suite de tests pour une méthode, pour une classe ou pour un logiciel.
Dans le cadre du cours, nous allons voir l’utilisation des tests unitaires.
Les tests unitaires
Dans un contexte objet, une suite de tests d’un test unitaire doit permettre de tester le maximum de fonctionnalités d’une classe. En général, on ne teste pas les procédures effectuant des entrées/sorties (lecture au clavier, écriture à l’écran, sauvegarde de fichier, etc.), mais ce n’est pas une règle absolue. S’il est possible de tester une méthode sans trop de difficulté, il est préférable de le faire. Les tests d’une série de tests doivent comprendre les types de cas suivants:
-
- Cas normaux:
- Représentent des cas qui permettent de valider que la méthode fait le travail qu’elle est sensée faire;
- Généralement les cas les plus simples à tester;
- Souvent, il s’agit du seul type de cas utilisés par les programmeurs plus « hatif ».
- Cas erronés:
- Représente des cas qui n’est pas sensé être traité par la méthode;
- Permet de valider la validation d’exceptions dans la méthode:
- Dans la plupart des langages, test l’utilisation des exceptions style throw.
- En Eiffel, ça permet de vérifier que les préconditions sont bien construits.
- Cas limites:
- Représente les premières valeurs représentées comme normales ou erronées.
- Généralement les cas les plus difficiles à trouver;
- Ce n’est pas toutes les méthodes qui ont des cas limites
- Par exemple, pour indexer une liste en Java, nous aurions quatre (4) cas limites:
- 0 qui représente le premier cas normal;
- -1 qui représente le dernier cas erroné (avant 0)
- « Taille de la liste -1 » qui représente le dernier cas normal
- « Taille de la liste » qui représente le premier cas erroné après la « Taille de la liste -1 »
- Par exemple, pour indexer une liste en Java, nous aurions quatre (4) cas limites:
- Cas normaux:
Exemples
Pour vous permettre de bien comprendre la mécanique, je vais donner un exemple de test unitaire pour un cas normal, erroné et limite en Java et Eiffel.
Les tests suivants s’appliquent à la méthode permettant de lire un élément dans une liste (en Java et en Eiffel).
Cas normaux
Voici un exemple de cas normal où nous utilisons une liste de 5 éléments et que nous utilisons l’index du second élément.
En Eiffel:
test_at_normal
-- Test normal pour {LIST}.`at'
note
testing: "covers/{LIST}.at"
local
l_liste:LIST[INTEGER]
do
create {LINKED_LIST[INTEGER]} l_liste.make
l_liste.extend (1)
l_liste.extend (2)
l_liste.extend (3)
l_liste.extend (4)
l_liste.extend (5)
assert("{LIST}.at(2) valide", l_liste.at (2) = 2)
end
En Java:
@Test
public void testGetNormal() {
List<Integer> lListe = new LinkedList<Integer>();
lListe.add(new Integer(1));
lListe.add(new Integer(2));
lListe.add(new Integer(3));
lListe.add(new Integer(4));
lListe.add(new Integer(5));
Assert.assertEquals(lListe.get(1).intValue(), 2);
}
Cas erronés
Voici un exemple de cas erroné où nous utilisons la même liste de 5 éléments et que nous utilisons l’index 10, qui est à l’extérieur de cette liste.
En Eiffel:
test_at_errone
-- Test erroné pour {LIST}.`at'
note
testing: "covers/{LIST}.at"
local
l_retry:BOOLEAN
l_liste:LIST[INTEGER]
l_valeur:INTEGER
do
if not l_retry then
create {LINKED_LIST[INTEGER]} l_liste.make
l_liste.extend (1)
l_liste.extend (2)
l_liste.extend (3)
l_liste.extend (4)
l_liste.extend (5)
l_valeur := l_liste.at (10)
assert("{LIST}.at erroné valide", False) -- N'est jamais sensé se rendre ici
end
rescue
if attached {PRECONDITION_VIOLATION} {EXCEPTIONS}.exception_manager.last_exception as
la_exception and then la_exception.type_name ~ Current.generating_type.name
then
l_retry := True
retry
end
end
La clause « rescue » sert en gros à la même chose qu’un « try…catch » en Java. En gros, si un appel de méthode effectue une exception lors de l’exécution, le code branche directement dans la clause « rescue ». Si le code se termine dans la clause « rescue », le test est considéré comme ayant échoué. C’est la raison pour laquelle on effectue un « retry », qui relance la méthode (sans modifier la variable « l_retry ») de manière à ce que la méthode termine correctement. Ce « retry » est lancé seulement si la méthode échoue avec une précondition, ce qui indique que la méthode gère correctement l’utilisation d’un mauvais index.
Sachez que le code suivant fonctionne tout le temps, pour valider les cas erronés. Vous pouvez donc utiliser directement ce code, en changeant seulement ce qui est à l’intérieur du « if not l_retry then ».
En Java:
@Test
public void testGetErrone() {
List<Integer> lListe = new LinkedList<Integer>();
lListe.add(new Integer(1));
lListe.add(new Integer(2));
lListe.add(new Integer(3));
lListe.add(new Integer(4));
lListe.add(new Integer(5));
try{
lListe.get(10);
Assert.assertTrue(false);
}catch(IndexOutOfBoundsException exception) {
}catch(Exception exception) {
Assert.assertTrue(false);
}
}
Cas limites normaux
Voici un exemple de cas limite normal où nous utilisons une liste de 5 éléments et que nous utilisons l’index du premier élément. Le premier élément est le premier index valide des cas normaux. C’est donc un cas limite.
En Eiffel:
test_at_limite_normal
-- Test limite normal pour {LIST}.`at'
note
testing: "covers/{LIST}.at"
local
l_liste:LIST[INTEGER]
do
create {LINKED_LIST[INTEGER]} l_liste.make
l_liste.extend (1)
l_liste.extend (2)
l_liste.extend (3)
l_liste.extend (4)
l_liste.extend (5)
assert("{LIST}.at(2) valide", l_liste.at (1) = 1)
end
En Java:
@Test
public void testGetNormalLimite() {
List<Integer> lListe = new LinkedList<Integer>();
lListe.add(new Integer(1));
lListe.add(new Integer(2));
lListe.add(new Integer(3));
lListe.add(new Integer(4));
lListe.add(new Integer(5));
Assert.assertEquals(lListe.get(0).intValue(), 1);
}
Cas limites erronés
Voici un exemple de cas limite erroné où nous utilisons une liste de 5 éléments et que nous utilisons l’index du premier élément. Le premier élément est le premier index valide des cas normaux. C’est donc un cas limite.
En Eiffel:
test_at_limite_errone
-- Test limite erroné pour {LIST}.`at'
note
testing: "covers/{LIST}.at"
local
l_retry:BOOLEAN
l_liste:LIST[INTEGER]
l_valeur:INTEGER
do
if not l_retry then
create {LINKED_LIST[INTEGER]} l_liste.make
l_liste.extend (1)
l_liste.extend (2)
l_liste.extend (3)
l_liste.extend (4)
l_liste.extend (5)
l_valeur := l_liste.at (0)
assert("{LIST}.at limite erroné valide", False) -- N'est jamais sensé se rendre ici
end
rescue
if attached {PRECONDITION_VIOLATION} {EXCEPTIONS}.exception_manager.last_exception as
la_exception and then la_exception.type_name ~ Current.generating_type.name
then
l_retry := True
retry
end
end
En Java:
@Test
public void testGetErroneLimite() {
List<Integer> lListe = new LinkedList<Integer>();
lListe.add(new Integer(1));
lListe.add(new Integer(2));
lListe.add(new Integer(3));
lListe.add(new Integer(4));
lListe.add(new Integer(5));
try{
lListe.get(-1);
Assert.assertTrue(false);
}catch(IndexOutOfBoundsException exception) {
}catch(Exception exception) {
Assert.assertTrue(false);
}
}
Outils de tests unitaires
Les outils de tests unitaires permettent d’automatiser l’exécution des tests. Ce type d’outil est très intéressant, car ils permettent d’exécuter rapidement et efficacement les tests unitaires du système entier, n’importe quand. Ce type d’outil permet généralement de:
-
- Écrire des tests;
- Lancer des tests;
- Avoir rapidement accès au rapport d’exécution des tests;
- Planifier l’exécution automatique des tests
La majorité des langages ont des outils de tests unitaires automatisés (par exemple: Junit, PHPunit, Auto test d’EiffelStudio, etc.)
Généralement, on aime exécuter l’entièreté des tests lorsque:
-
- On a terminé une fonctionnalité importante et on s’apprête à faire une soumission sur notre dépôt de gestion de version (par exemple, git);
- On a effectué un « merge » de plusieurs branches sur notre dépôt de gestion de version et on doit le soumettre officiellement;
- On doit créer une « release » finale (envoyer l’exécutable sur le site web de la compagnie par exemple).
Références
Voici la référence de Eiffel AutoTest: https://www.eiffel.org/doc/eiffelstudio/AutoTest
Explication d’utilisation de Eiffel AutoTest: https://www.eiffel.org/doc/eiffelstudio/Using_AutoTest
Auteur: Louis Marchand
Sauf pour les sections spécifiées autrement, ce travail est sous licence Creative Commons Attribution 4.0 International.