📘 Concepts gĂ©nĂ©raux

Le test logiciel est une approche de vĂ©rification dynamique qui repose sur l'exĂ©cution d’un logiciel − ou d’une partie d’un logiciel − afin d’en observer diffĂ©rents comportements. Par de telles observations il est possible de dĂ©tecter des dĂ©faillances, et donc d’en dĂ©duire la prĂ©sence de dĂ©fauts − autrement dit un non respect de la spĂ©cification.

Étant donnĂ© qu’un logiciel peut avoir un trĂšs grand nombre de comportements possibles − notamment en fonction de ses donnĂ©es d’entrĂ©e ou de la complexitĂ© de son code − le test logiciel repose sur l’exĂ©cution d’un mĂȘme logiciel un grand nombre de fois afin d’en observer tous les comportements estimĂ©s significatifs.

Par la suite, on dénommera logiciel testé le logiciel (ou la partie du logiciel) que nous souhaitons tester.

1. Définitions et concepts essentiels

1.1. Environnement d’exĂ©cution d’un logiciel

Un logiciel s’exĂ©cute toujours dans un environnement donnĂ©, que l’on peut dĂ©finir comme tout ce qui peut affecter le comportement du logiciel lors de son exĂ©cution.

entrees sorties.drawio
Figure 1. Environnement d’exĂ©cution d’un logiciel

L’environnement d’exĂ©cution d’un logiciel (ou s’une partie d’un logiciel) va notamment comprendre :

  • L’espace mĂ©moire allouĂ© au logiciel, dans lequel ce dernier va lire et/ou Ă©crire :

    • son Ă©tat (valeurs des variables, contenu des objets, Ă©tat de la pile d’exĂ©cution, etc),

    • les arguments confiĂ©s au logiciel lors de son lancement,

    • les valeurs de retour produites par son exĂ©cution.

  • Le systĂšme d’exploitation, dans lequel le logiciel pourra interagir notamment avec :

    • l’ensembles des fichiers et dossiers,

    • d’autres logiciels en cours d’exĂ©cution (processus worker, base de donnĂ©es, etc.).

  • Le monde physique, dans lequel le logiciel pourra interagir notamment avec :

    • des pĂ©riphĂ©riques d’entrĂ©e (claviers, souris, manettes, microphones, capteurs),

    • des pĂ©riphĂ©riques de sortie (Ă©cran, haut-parleurs, moteurs, actuateurs),

    • des rĂ©seaux informatiques, permettant de contacter Ă  distances d’autres logiciels en exĂ©cution

Un logiciel en exĂ©cution va accĂ©der Ă  une partie de son environnement afin de rĂ©cupĂ©rer un ensemble de donnĂ©es que nous appellerons donnĂ©es d’entrĂ©e. Ces donnĂ©es permettent au logiciel de se configurer et de fonctionner, et font lui amener Ă  exprimer un comportement particulier.

De mĂȘme, un logiciel en exĂ©cution va produire un effet qui va affecter une partie de son environnement. On appellera donnĂ©es de sorties l’ensemble des changements et effets produits par un comportement du logiciel.

1.2. Scénario de test

Ainsi, pour vĂ©rifier qu’un comportement d’un logiciel est bien celui attendu par sa spĂ©cification, on peut procĂ©der de la maniĂšre suivante :

  1. On prĂ©pare un ensemble de donnĂ©es d’entrĂ©e,

  2. On exĂ©cute le logiciel avec les donnĂ©es d’entrĂ©e prĂ©parĂ©es,

  3. On observe et analyser les données de sorties produits par le comportement résultant.

Puisqu’il est nĂ©cessaire de vĂ©rifier possiblement un grand nombre de comportements chez un logiciel, il est nĂ©cessaire de pouvoir rigoureusement reproduire ce processus un grand nombre de fois. C’est le principe du scĂ©nario de test (aussi appelĂ© cas de test ou simplement test), Ă  savoir une sĂ©quence d’actions claire et reproductible visant Ă  vĂ©rifier un comportement spĂ©cifique d’un logiciel. Un scĂ©nario de test est gĂ©nĂ©ralement composĂ© des Ă©lĂ©ments suivants :

  • Un ensemble de donnĂ©es d’entrĂ©es qui seront confiĂ©es au logiciel (ex. arguments, fichiers, entrĂ©es dans une base de donnĂ©e, Ă©tat, sĂ©quence d’Ă©vĂšnements dans une interface graphique), qui dĂ©finissent l’ensemble de l’environnement nĂ©cessaire pour Ă©tudier un comportement.

  • Une procĂ©dure d’exĂ©cution du logiciel avec les donnĂ©es d’entrĂ©e, qui devra Ă©galement nous permettre de capturer les donnĂ©es de sortie que nous souhaitons observer.

  • Un oracle, qui est une procĂ©dure capable d’analyser les donnĂ©es de sortie, et de dĂ©cider si oui ou non une dĂ©faillance est apparue dans le comportement observĂ©.

On appelle suite de tests un ensemble de scénarios de test pour un logiciel testé.

Exemple 1. Premier scénario de test pour PALINDROME

On reprend le cas du logiciel PALINDROME prĂ©sentĂ© prĂ©cĂ©demment. Pour rappel, sa spĂ©cification nous indique que le logiciel s’exĂ©cute en ligne de commande, prend une chaĂźne de caractĂšres en paramĂštre, et doit afficher "Oui" si la chaĂźne est un palindrome, et "Non" dans le cas contraire.

Pour vĂ©rifier une partie de cette spĂ©cification, on propose le scĂ©nario de test suivant :

  • DonnĂ©e d’entrĂ©e : "anna"

  • ProcĂ©dure d’exĂ©cution du logiciel : java -jar palindrome.jar "anna"

  • Oracle : Lecture de la sortie console, le message "Oui" doit ĂȘtre prĂ©sent.

Enfin, l’exĂ©cution d’un scĂ©nario de test suit le processus suivant :

execution scenario.drawio
  1. À l’aide des donnĂ©es de test et de la procĂ©dure d’exĂ©cution, on exĂ©cute le logiciel testĂ© (ou une partie du logiciel). Deux possibilitĂ©s peuvent suivre :

    1. Si cette exĂ©cution n’arrive pas jusqu’au bout, le test est considĂ©rĂ© comme Ă©tant en erreur, ce qui correspond presque toujours au non-respect d’une contrainte du type "le logiciel ne doit pas planter".

    2. Si cette exĂ©cution arrive au bout, elle produit un comportement observable, que ce soit par la production de nouvelles donnĂ©es, le changement de certaines variables, ou des effets de bord sur le systĂšme (fichiers modifiĂ©s, base de donnĂ©e altĂ©rĂ©e, etc.). On capture ces observations dans ce qu’on appelle les sorties.

  2. Enfin, si le comportement a pu ĂȘtre observĂ© et qu’il n’y a pas eu d’erreur, on fournit les sorties Ă  l'oracle, qui va les analyser et prendre une dĂ©cision :

    1. Si le comportement observé correspond à la spécification, alors le test est considéré comme une réussite. On dit aussi que le teste "passe".

    2. Sinon, le test est un échec.

En Java, une erreur sera typiquement provoquĂ©e par une exception de type RuntimeException, les plus connues Ă©tant NullPointerException et IndexOutOfBoundsException. Pour rappel, en Java, une RuntimeException est un type particulier d’exception qui peut se produire mĂȘme si la mĂ©thode qui l’Ă©met ne la dĂ©clare pas dans sa signature Ă  l’aide du mot-clĂ© throws.
Des exemples d’exĂ©cution de scĂ©nario de test sont donnĂ©s dans la section qui suit.

1.3. Test manuel, test automatisé

L’exĂ©cution d’un scĂ©nario de test se rĂ©alise gĂ©nĂ©ralement soit de maniĂšre manuelle soit de maniĂšre automatisĂ©e.

Le test manuel fait intervenir une personne responsable de l’application de chaque Ă©tape (saisir une valeur au clavier, cliquer quelque part, attendre un certain temps, suivre l’oracle, etc.) du scĂ©nario de test. Dans cette situation, chaque scĂ©nario de test est simplement dĂ©crit en langue naturelle, parfois Ă  l’aide de tableaux ou de listes. Le test manuel correspond Ă©galement ce qu’on dĂ©veloppeur fait quotidiennement pour s’assurer que le logiciel qu’il rĂ©alise correspond bien Ă  ses attentes : il le lance, effectue des manipulations choisies, et observe que tout se passe comme prĂ©vu.

Cependant, le test manuel possĂšde ces principaux dĂ©fauts :

  • Il nĂ©cessite Ă  des personnes (ex. des dĂ©veloppeurs) de prendre du temps pour exĂ©cuter les scĂ©narios de test, ce qui reprĂ©sente un coĂ»t considĂ©rable.

  • Il est pĂ©nible et laborieux pour une personne de rĂ©-exĂ©cuter un scĂ©nario de test Ă  de nombreuses reprises, alors que c’est une tĂąche nĂ©cessaire tout au long du dĂ©veloppement et de la vĂ©rification (voir schĂ©ma au dĂ©but de ce chapitre).

  • Certains logiciels ou certaines parties de logiciels sont destinĂ©(e)s uniquement Ă  d’autres logiciels − ce qu’on appelle couramment des bibliothĂšques logicielles − et ne sont pas directement utilisables et donc difficilement testables Ă  la main par des humains.

Exemple 2. Exécution manuelle du scénario de test précédent

Pour le mĂȘme logiciel d’analyse de palindrome, on exĂ©cute manuellement le scĂ©nario de test vu dans l’exemple prĂ©cĂ©dent :

  • On ouvre un terminal textuel et on se rend dans le rĂ©pertoire avec le programme compilĂ©,

  • On saisit au clavier java -jar palindrome.jar "anna" et on valide,

  • On applique l’oracle en regardant l’Ă©cran : "Oui" est bien affichĂ©, c’est donc une rĂ©ussite.

À l’inverse, le test automatisĂ© consiste Ă  faire exĂ©cuter le scĂ©nario de test par une machine et non par une personne. Il s’effectue en deux Ă©tapes :

  1. Chaque scĂ©nario de test est implĂ©mentĂ© sous la forme d’un script qui se charge de chacune des Ă©tapes sans intervention humaine (initialisation, exĂ©cution, oracle). Un tel script est un logiciel dont le but est d’exĂ©cuter et d’observer un autre logiciel.

  2. Ensuite, tous les scripts de test sont exécutés et les résultats obtenus sont rassemblés.

Le test automatisĂ© a pour inconvĂ©nient la nĂ©cessitĂ© de programmer les scĂ©narios de test, ce qui prend du temps et ce qui n’est pas toujours trivial (ex. simuler les interactions avec une interface graphique est parfois difficile.). Cependant, le test automatisĂ© rĂ©pond Ă  tous les problĂšmes du test manuel : l’exĂ©cution des tests ne demande pas de temps humain ; la rĂ©-exĂ©cution des scĂ©narios de test est facile ; on peut tester des bibliothĂšques logicielles beaucoup plus facilement.

Exemple 3. Exécution automatisée du scénario de test précédent

Pour le mĂȘme logiciel d’analyse de palindrome, on implĂ©mente le scĂ©nario de test vu prĂ©cĂ©demment sous la forme du script de test suivant, Ă©crit avec le langage Bash :

# Exécution du logiciel avec donnée d'entrée
RESULT=$(java -jar palindrome.jar "anna")

# Oracle
if [[ "$RESULT" == "Oui" ]]; then
  echo "Test [\"anna\", Oui] réussi"
else
  echo "Test [\"anna\", Oui] échoué"
fi

Puis on exĂ©cute le script de test de la sorte :

$ bash ./test-case.sh
Test ["anna", Oui] réussi

On constate que le test est bien là aussi une réussite.

Dans le reste de ce cours on s’intĂ©ressera uniquement aux tests automatisĂ©s.

1.4. Domaines d’entrĂ©e et de sortie d’un logiciel

Concevoir un scĂ©nario de test requiert une bonne comprĂ©hension des donnĂ©es d’entrĂ©e et de sortie du logiciel testĂ©. Cette comprĂ©hension peut s’obtenir en identifiant les domaines d’entrĂ©e et de sortie d’un logiciel Ă  l’aide d’une lecture attentive de la spĂ©cification. Un domaine d’entrĂ©e ou de sortie prend la forme d’un ensemble de variables, chaque variable possĂ©dant un domaine de valeurs et pouvant possĂ©der un nom. Ainsi :

  • Un domaine d’entrĂ©e se dĂ©finit par un ensemble de variables d’entrĂ©es (souvent appelĂ©s paramĂštres d’entrĂ©e). Conceptuellement, c’est le scĂ©nario de test qui affecte des valeurs (aussi appelĂ©es des arguments) Ă  ces variables pour influencer l’exĂ©cution du logiciel et l’amener Ă  produire un certain comportement.

  • Un domaine de sortie se dĂ©finit par un ensemble de variables de sortie. Conceptuellement, c’est le logiciel qui modifie ou affecte les valeurs de ces variables, qui pourront ensuite ĂȘtre analysĂ©es par l’oracle du scĂ©nario de test.

Le mot variable ici ne dĂ©signe pas uniquement une variable au sens "programmation". En effet, une variable d’entrĂ©e peut trĂšs bien ĂȘtre un fichier ou une table de base de donnĂ©es, tandis qu’une variable de sortie peut trĂšs bien ĂȘtre un envoi d’exception ou le contenu visuel d’une page web.
Exemple 4. Domaines d’entrĂ©e et de sortie d’un logiciel de recherche dans un fichier

On considĂšre un logiciel permettant de chercher une chaĂźne de caractĂšres au sein d’un fichier texte. Il existe une option pour indiquer au logiciel si on souhaite ignorer ou non la casse des caractĂšres lors de la recherche. Une fois terminĂ©, le logiciel peut soit nous indiquer que le mot est bien prĂ©sent, soit que le mot est bien absent, soit qu’il y a une erreur dans la lecture du fichier (ex. fichier absent).

On reprĂ©sente le domaine d’entrĂ©e de ce logiciel prend Ă  l’aide de trois paramĂštres d’entrĂ©e :

  •  : le fichier Ă  analyser – le domaine de ce paramĂštre d’entrĂ©e est "l’ensemble des fichiers",

  •  : la chaĂźne de caractĂšres Ă  rechercher dans le fichier – le domaine de ce paramĂštre d’entrĂ©e est "l’ensemble des chaĂźnes de caractĂšres",

  •  : le choix d’ignorer la casse des caractĂšres ou non – le domaine de ce paramĂštre d’entrĂ©e est l’ensemble des boolĂ©ens, donc {vrai, faux}.

L’ensemble des valeurs possible de ce domaine d’entrĂ©e est le produit cartĂ©sien des domaines des diffĂ©rentes variables, Ă  savoir .

Cet ensemble de valeurs est conceptuellement infini, puisqu’il existe une infinitĂ© de fichiers possibles, ainsi qu’une infinite de chaĂźnes de caractĂšres possibles. En pratique, ce domaine est simplement extrĂšmement grand, puisque, sur un ordinateur donnĂ© et dans un environnement logiciel donnĂ©, un fichier possĂšde une taille maximale, de mĂȘme qu’une chaĂźne de caractĂšres.

Un exemple de donnĂ©es d’entrĂ©es conforme Ă  ce domaine d’entrĂ©e serait :

  •  : fichier contenant "Bonjour Ă  tous"

  •  : la chaĂźne "tous"

  •  : faux

Enfin, on représente le domaine de sortie avec une unique variable de sortie :

  •  : le rĂ©sultat de la recherche – le domaine est l’ensemble {prĂ©sente, absente, erreur fichier}.

Signatures de fonctions typées

Si un logiciel testĂ© est une fonction (aussi appelĂ©e procĂ©dure) dĂ©veloppĂ© Ă  l’aide d’un langage de programmation typĂ©, il est possible d’identifier une partie des domaines d’entrĂ©e et de sortie en s’intĂ©ressant Ă  la signature de la fonction :

  • la liste des paramĂštres nous indique un ensemble de variables d’entrĂ©e, et le type d’un paramĂštre nous indique le domaine de la variable d’entrĂ©e,

  • le type de retour nous indique le domaine d’une variable de retour.

Par exemple si on regarde la signature de fonction suivante en TypeScript :

function areEqual(x: number, y: number): boolean { 
 }

On peut en déduire :

  • une variable d’entrĂ©e x avec pour domaine l’ensemble des flottants,

  • une variable d’entrĂ©e y avec pour domaine l’ensemble des flottants,

  • une variable de sortie avec pour domaine l’ensemble {true, false}.

Cependant une signature ne permet pas de dĂ©terminer l’intĂ©gralitĂ© des domaines, car une fonction peut Ă©galement lire ou Ă©crire d’autres donnĂ©es dans son environnement (ex. des fichiers), qui font alors par dĂ©finition Ă©galement partie du domaine. Une lecture attentive de la spĂ©cification de la fonction reste donc nĂ©cessaire !

Le cas d’une mĂ©thode en orientĂ© objet est un peu diffĂ©rent d’une fonction, voir la section dĂ©diĂ©e.
Exemple 5. Domaines d’entrĂ©e et de sortie de PALINDROME

On considĂšre le mĂȘme logiciel PALINDROME que dans les chapitres prĂ©cĂ©dents, Ă  savoir un logiciel en Java qui s’exĂ©cute en ligne de commande et qui peut dĂ©tecter si un mot est un palindrome ou non. Plus prĂ©cisĂ©ment, on s’intĂ©ressera ici directement au service Java principal requis dans le logiciel. Selon la spĂ©cification, ce service doit implĂ©menter l’interface Java suivante :

package app;

interface Palindrome {
    /**
     * Verifies that a word is a palindrome.
     * Ignores the character case.
     * 
     * @return true if word is a palindrome, false otherwise.
     * @throws NullPointerException if word is null
     * @throws IllegalArgumentException if word is empty 
     *                                  or contains a digit, 
     *                                  or contains a special character.
     **/
    boolean isPalindrome(String word) throws IllegalArgumentException, NullPointerException;
}

Concernant le domaine d’entrĂ©e :

  • On constate dans la spĂ©cification et dans la signature un paramĂštre word avec pour type String.

  • On sait qu’en Java qu’une variable avec pour type String peut Ă©galement prendre pour valeur null.

  • On en dĂ©duit que le domaine d’entrĂ©e est composĂ© d’une unique variable d’entrĂ©e qu’on nomme word avec pour domaine String âˆȘ { null }

Concernant le domaine de sortie :

  • On constate dans la spĂ©cification et dans la signature un type de retour boolean.

  • On constate dans la spĂ©cification et dans la signature qu’un envoi d’exception soit de type NullPointerException, soit de type IllegalArgumentException est possible.

  • On en dĂ©duit que le domaine de sortie est composĂ© d’une unique variable de sortie qu’on nomme result avec pour domaine {true, false, IllegalArgumentException, NullPointerException}

On peut aussi représenter le domaine de sortie sous la forme de deux variables :

  • return_value avec pour domaine { true, false, − }

  • exception avec pour domaine { IllegalArgumentException, NullPointerException, − }

avec '−' une valeur qui dĂ©note l’absence de valeur.

1.5. Domaines d’entrĂ©e et sortie en orientĂ© objet

En cas de programmation orientĂ©e objet, lorsque l’on teste une mĂ©thode, il ne faut pas oublier de considĂ©rer l’objet appelant comme faisant partie des paramĂštres d’entrĂ©e ! En effet, les valeurs des diffĂ©rents attributs de l’objet peuvent ĂȘtre accĂ©dĂ©es par la mĂ©thode et peuvent donc influencer son comportement.

Domaine d’entrĂ©e d’une mĂ©thode avec un seul objet

Soit une interface java IntegerList et la mĂ©thode add :

public interface IntegerList {
    boolean add(Integer element);
}

La mĂ©thode add n’Ă©tant pas statique, son domaine d’entrĂ©e est IntegerList Integer

Et si on effectue un appel de cette mĂ©thode sur une IntegerList nommĂ©e l1 :

l1.add(-5);

les donnĂ©es d’entrĂ©es confiĂ©es Ă  add sont ici bien {l1, -5}

Ajoutons Ă  cela deux constats importants :

  • Chaque attribut d’un objet peut ĂȘtre considĂ©rĂ© comme un possible paramĂštre d’entrĂ©e de la mĂ©thode testĂ©e.

  • Tout objet accessible directement (l’objet appelant ou bien un objet passĂ© en paramĂštre) ou indirectement (au travers des attributs des autres objets accessibles) est un possible paramĂštre d’entrĂ©e, ainsi que chacun de ses attributs.

  • Si les objets sont manipulĂ©s par des pointeurs (comme en Java), tout objet ou attribut peut Ă©galement ĂȘtre une variable de sortie ! En effet, les objets d’entrĂ©e (l’objet appelant ainsi que les objets passĂ©s en paramĂštre) peuvent tous ĂȘtre modifiĂ©s par la mĂ©thode, ce qui contribue Ă  changer l’Ă©tat du systĂšme testĂ©.

Domaines d’entrĂ©e et de sortie d’une mĂ©thode avec plusieurs objets

Soit le programme objet suivant :

Diagram

Le domaine d’entrĂ©e de la mĂ©thode do est possiblement composĂ© des paramĂštres d’entrĂ©e suivants :

  • mode int

  • this.x int

  • this.b B {null}

  • this.b.y bool {_}

  • this.b.c C {null, _}

  • this.b.c.z string {null, _}

oĂč la valeur null reprĂ©sente l’absence d’un objet sur un pointeur (par exemple l’absence d’une instance B dans this.b) et oĂč la valeur vide _ dĂ©note le cas oĂč un Ă©lĂ©ment est inaccessible (par exemple y n’est pas accessible sans objet B dans this.b).

Seuls les attributs qui influencent vraiment le comportement de la mĂ©thode doivent ĂȘtre identifiĂ©s comme des paramĂštres d’entrĂ©e.

Et le domaine de sortie de la méthode do est possiblement composé des variables de sortie suivantes :

  • result bool

  • this.x int

  • this.b B {null}

  • this.b.y bool {_}

  • this.b.c C {null, _}

  • this.b.c.z string {null, _}

Malheureusement, le principe de l’encapsulation fait qu’il est courant de manipuler un objet uniquement au travers d’une interface, et donc de ne pas connaĂźtre ses attributs.

Dans ce cas, toute mĂ©thode permettant d’observer des donnĂ©es encapsulĂ©es (getters notamment, mais pas uniquement) pourra possiblement ĂȘtre considĂ©rĂ©e comme une variable d’entrĂ©e ou de sortie, de maniĂšre similaire Ă  un attribut. La documentation de l’interface sera une bonne source d’informations pour bien comprendre un contexte donnĂ©.

Domaines d’entrĂ©e et de sortie d’une mĂ©thode avec plusieurs objets avec getters
Diagram

Le domaine d’entrĂ©e de la mĂ©thode do est possiblement composĂ© des paramĂštres d’entrĂ©e suivants :

  • mode int

  • this.getX() int

  • this.getB() B {null}

  • this.getB().getY() bool {_}

  • this.getB().getC() C {null, _}

  • this.getB().getC().getZ() string {null, _}

Et le domaine de sortie de la méthode do est possiblement composé des variables de sortie suivantes :

  • result bool

  • this.getX() int

  • this.getB() B {null}

  • this.getB().getY() bool {_}

  • this.getB().getC() C {null, _}

  • this.getB().getC().getZ() string {null, _}

1.6. Tableau de scénarios

Dans ce cours, on reprĂ©sentera souvent une suite de tests sous la forme d’un tableau de scĂ©narios, dont les colonnes reprĂ©sentent les donnĂ©es d’entrĂ©e et l’oracle, et dont les lignes reprĂ©sentent les scĂ©narios de test que comporte la suite de tests.

Pour présenter cette notation, supposons une méthode avec la signature de la forme suivante :

public int foo(int x, boolean y, String z) throws SpecificException

On suppose ici que les seules donnĂ©es d’entrĂ©e pertinentes sont x, y et z, et que les seules donnĂ©es de sortie pertinentes sont la valeur de retour (de type int), l’exception (de type SpecificException) et l’objet appelant (this).

Un tableau de scénarios pour cette méthode prend alors la forme suivante :

Tableau 1. Structure d’un tableau des scĂ©narios
DonnĂ©es d’entrĂ©e Oracle

x : int

y : bool

z : string

result

SpecificException

this.contains(4)

1

2

false

"message"

45

true

2

-1

false

"message"

-

×

-

3

2

true

"message"

9

false

dans lequel :

  • Chaque colonne reprĂ©sente soit une variable d’entrĂ©e, soit une variable de sortie.

  • Chaque ligne reprĂ©sente un scĂ©nario de test (qui portera le mĂȘme nom que la combinaison correspondante). Dans chaque ligne, on inscrira les valeurs concrĂštes choisies pour chaque paramĂštre d’entrĂ©e (de maniĂšre Ă  satisfaire les choix rĂ©alisĂ©s pour cette combinaison), et on inscrira quelles valeurs sont attendues pour les diffĂ©rentes donnĂ©es de sortie possibles.

  • Pour les donnĂ©es de sortie, une colonne pourra notamment indiquer :

    • une valeur attendue pour une donnĂ©e de sortie − par exemple result, qui par convention sera la valeur retournĂ©e par une mĂ©thode,

    • l’absence ou la prĂ©sence d’une sortie particuliĂšre − par exemple une exception, dont la prĂ©sence pourra ĂȘtre indiquĂ©e par une simple croix,

    • la satisfaction d’une expression boolĂ©enne donnĂ©e.

Exemple 6. Premier tableau de scénarios pour isPalindrome

On prépare une premiÚre suite de tests pour isPalindrome sous la forme du tableau suivant :

ParamĂštres d’entrĂ©e Oracle

word : string

return_value

exception

1

"bonjour!"

−

IllegalArgumentException

2

"john"

false

−

3

"anna"

true

−

2. Processus général du test logiciel

De maniĂšre gĂ©nĂ©rale, une activitĂ© de test logiciel revient Ă  suivre le processus suivant :

process.drawio
Figure 2. Processus gĂ©nĂ©ral d’une activitĂ© de test logiciel
  • Supposons la rĂ©alisation d’un logiciel, Ă  savoir le logiciel testĂ©. Cette rĂ©alisation nĂ©cessite des changements du code du logiciel, Ă  savoir des ajouts ou des modifications de parties existantes.

  • Comme tout changement de code est susceptible d’introduire un dĂ©faut dans le logiciel testĂ©, on entreprend de tester le logiciel et ses changements Ă  l’aide de scĂ©narios de test.

    • Si aucun scĂ©nario de test n’existe, on dĂ©bute une conception de scĂ©narios de test Ă  l’aide d’une approche de test. En outre, si on souhaite faire du test automatisĂ©, alors s’ajoute Ă  cela une Ă©tape de dĂ©veloppement, oĂč on implĂ©mente les scĂ©narios de test sous forme de scripts de tests.

    • Sinon, on se saisit simplement des scĂ©narios de test existants.

  • Puis on lance une exĂ©cution de l’ensemble des scĂ©narios de tests (anciens comme nouveaux) sur le logiciel testĂ©.

    • Si l’un des scĂ©narios de test ne passe pas (Ă©chec ou erreur), cela peut signifier qu’une dĂ©faillance a Ă©tĂ© observĂ©e. On doit alors rĂ©aliser un dĂ©bogage pour dĂ©couvrir le dĂ©faut qui cause de la dĂ©faillance, pour ensuite le corriger, et repartir au point de dĂ©part du processus.

    • Si tous les scĂ©narios de test passent (rĂ©ussites), il ne reste qu’Ă  effectuer une mesure de la qualitĂ© de la suite de tests, afin de dĂ©terminer si des tests additionnels sont nĂ©cessaires. Cette mesure dĂ©pend de l’approche de test employĂ©e, et consiste Ă  mesurer la proportion de cibles atteintes par les tests (combinaisons, lignes de code, dĂ©fauts potentiels, etc).

      • Si la qualitĂ© est considĂ©rĂ©e insuffisante pour les objectifs fixĂ©s, alors on repart en conception de scĂ©narios de test.

      • Si la qualitĂ© est estimĂ©e suffisante, l’activitĂ© de test est terminĂ©e (pour le moment !).

Différentes approches de test seront présentées dans de futurs chapitres du cours.
Il est possible (et mĂȘme recommandĂ©) d’utiliser conjointement diffĂ©rentes approches de test, que ce soit lors de la conception ou de la mesure de la qualitĂ© des tests.

3. Classification du test logiciel

Il existe dans la littĂ©rature un grand nombre d’adjectifs associĂ©s au mot test permettant de catĂ©goriser les diffĂ©rentes activitĂ©s du test logiciel et/ou les diffĂ©rents types de scĂ©narios de test en test logiciel. Dans cette section nous passons non exhaustivement en revue cette dense classification, en se concentrant sur quelques maniĂšres courantes de catĂ©goriser le test logiciel.

3.1. Selon l’accĂšs au code source

En fonction du contexte ou de l’objectif, la personne responsable de l’Ă©criture des tests (testeur ou testeuse) peut reposer sur diffĂ©rentes sources d’information pour la conception de scĂ©narios de test. Le code source du logiciel testĂ© fait partir des possibles sources d’informations que l’on peut (ou non) utiliser, et on distingue communĂ©ment trois cas :

  • On parle de test boĂźte blanche lorsqu'on utilise principalement le code source de ce qui est testĂ© lors de la conception des scĂ©narios de test. En analysant la structure du code et des composants internes, on peut par exemple en dĂ©duire quels scĂ©narios de test sont nĂ©cessaires pour que chaque composant interne soit testĂ©.

    Tester en boĂźte blanche nĂ©cessite tout de mĂȘme de lire et de considĂ©rer la spĂ©cification du logiciel testĂ©, notamment afin de concevoir l'oracle de chaque scĂ©nario de test.
    L’approche structurelle et l’approche par mutation, prĂ©sentĂ©es dans de prochains chapitres, sont deux approches de conception de tests de type boĂźte blanche.
  • On parle de test boĂźte noire lorsqu'on n’utilise pas le code source de ce qui est testĂ© lors de la conception des scĂ©narios de test. Ce terme est souvent utiliser pour qualifier les approches qui reposent principalement sur la spĂ©cification du logiciel testĂ© pour dĂ©terminer quels scĂ©narios de test concevoir.

    L’approche combinatoire, prĂ©sentĂ©e dans un prochain chapitre, est une approche de conception de tests de type boĂźte noire.

3.2. Selon la propriété vérifiée

La spĂ©cification du logiciel testĂ© dĂ©finit l’ensemble des propriĂ©tĂ©s (ou contraintes) que doit satisfaire le logiciel. On distingue communĂ©ment deux activitĂ©s de test en fonction du type de propriĂ©tĂ©s testĂ©es :

  • On parle de test fonctionnel lorsqu’on cherche Ă  vĂ©rifier qu’une fonctionnalitĂ© attendue (ex. faire un calcul, rĂ©agir Ă  une action, produire un rĂ©sultat, afficher un Ă©lĂ©ment) du logiciel testĂ© correspond bien aux attentes, ce qui impliquer vĂ©rifier que les comportements du logiciel permettent bien d’accomplir les tĂąches prĂ©vues. Le test fonctionnel reprĂ©sente gĂ©nĂ©ralement l’essentiel de l’activitĂ© de test logiciel.

    Généralement le test fonctionnel est réalisé en boite noire
  • On parle de test non-fonctionnel lorsqu’on cherche Ă  vĂ©rifier ce qui n’est pas directement liĂ© aux fonctionnalitĂ©s. Dans cette grande catĂ©gorie on peut citer le test de robustesse (est-ce que le logiciel peut gĂ©rer des cas peu prĂ©vus ?), le test de performance (est-ce que le logiciel utilise beaucoup de CPU ? de mĂ©moire ? est-ce que les calculs sont rapides ?), le test de charge (est-ce que le logiciel peut endurer de trĂšs nombreuses sollicitations ?), le test de compatibilitĂ© (est-ce que le logiciel peut fonctionner sur des plateformes diffĂ©rentes ?), le test de sĂ©curitĂ© (est-ce que le logiciel possĂšde des failles de sĂ©curitĂ© ? peut rĂ©sister Ă  certaines attaques ?)

    Généralement le test non-fonctionnel est réalisé en boite blanche
    La frontiĂšre entre fonctionnel et non-fonctionnel n’est pas toujours clairement dĂ©finie. Par exemple il est difficile de classer certaines contraintes liĂ©s Ă  la sĂ©curitĂ© ou Ă  l'ergonomie dans une seule des deux catĂ©gories.
Dans ce cours on ne s’intĂ©ressera que au test fonctionnel.

3.3. Selon l’usage des tests

Une autre catĂ©gorisation repose sur l’usage qui est fait des scĂ©narios de test.

  • On parle de test de rĂ©gression (aussi appelĂ© test de non-regression) lorsque l’exĂ©cution des scĂ©narios de test vise Ă  dĂ©tecter l’apparition de rĂ©gressions, Ă  savoir de nouveaux dĂ©fauts apparus suite Ă  des changements rĂ©alisĂ© dans un logiciel. Cela nĂ©cessite l’exĂ©cution des tests Ă  chaque changement rĂ©alisĂ©, ou Ă  chaque version produite, ce qui peut ĂȘtre automatisĂ© Ă  l’aide d’un gestionnaire de version (comme Git) couplĂ© Ă  un exĂ©cution des tests au sein d’une intĂ©gration continue (comme Gitlab CI).

  • On parle de test d’acceptation (aussi appelĂ© test de recette) lorsque l’exĂ©cution des tests vise Ă  valider que l’entiĂšretĂ© du logiciel testĂ© est conforme aux attentes du client. Ce type de test ne concerne que les scĂ©narios de test conçus directement sur la base des besoins du clients Ă©tablis dans le cahier des charges initial.

    GĂ©nĂ©ralement le test d’acceptation prend la forme de test systĂšme (voir plus bas).
  • Un peu Ă  part, on parle de dĂ©veloppement dirigĂ© par les tests lorsque les scĂ©narios de test sont conçus progressivement avant le dĂ©veloppement de chaque nouvelle fonctionnalitĂ©, puis sont exĂ©cutĂ©s lorsque la dite fonctionnalitĂ© est implĂ©mentĂ©e. Cette approche permet non seulement de garantir que toute fonctionnalitĂ© est toujours testĂ©e, mais permet aussi d’aider dans la conception des interfaces des composants d’un logiciel.

3.4. Selon le niveau de test

Une importante classification du test logiciel se fait en fonction de l’Ă©chelle, ou de la dimension, d’un scĂ©nario de test vis Ă  vis du logiciel testĂ©. Dit autrement, un scĂ©nario de test sera forcĂ©ment spĂ©cifique Ă  une partie, plus ou moins large, d’un logiciel. C’est ce qu’on appelle les niveaux du test logiciel.

ConsidĂ©rons d’abord la situation habituelle : nous souhaitons tester un systĂšme logiciel, et ce dernier est censĂ© respecter une spĂ©cification. Cette spĂ©cification dĂ©clare quelles sont les entrĂ©es possibles pour ce systĂšme logiciel et quelles sorties ce systĂšme est censĂ© produire en consĂ©quence . SchĂ©matiquement :

system.drawio
Figure 3. SystÚme logiciel et sa spécification

Mais un logiciel n’est que rarement constituĂ© d’un bloc monolithique de code. En pratique, il est toujours dĂ©composable en un ensemble d'unitĂ©s (aussi appelĂ©es modules ou composants) qui sont intĂ©grĂ©es ensemble afin de permettre au systĂšme de produire les comportements attendus. De telles unitĂ©s peuvent ĂȘtre des fonctions, des mĂ©thodes, des classes ou encore des packages, en fonction de la granularitĂ© considĂ©rĂ©e. Pour un langage procĂ©dural comme C, l’unitĂ© communĂ©ment considĂ©rĂ©e est la fonction, tandis que pour un langage orientĂ© objet comme Java ce sera la plus souvent la mĂ©thode (ou Ă©ventuellement la classe).

Lors de la conception d’un logiciel, une des Ă©tapes consiste Ă  dĂ©cider de comment dĂ©composer le systĂšme en unitĂ©s, et quels seront les services rendus par ces diffĂ©rentes unitĂ©s. En d’autres termes, on Ă©tablit pour chaque unitĂ© une spĂ©cification qui lui est propre au sein des documents de conceptions. Ces spĂ©cifications peuvent prendre par exemple la forme de modĂšles UML (diagrammes des classes, de sĂ©quence, d’Ă©tats-transitions) ou encore directement de code commentĂ© en dĂ©tail dĂ©finissant des interfaces logicielles (par exemple une interface Java).

Si on Ă©tend le schĂ©ma prĂ©cĂ©dent, on obtient alors :

system to units.drawio
Figure 4. SystÚme logiciel et unités
La spĂ©cification d’une unitĂ© est rarement rĂ©alisĂ©e avec le client du logiciel produit, mais principalement par les concepteurs et les dĂ©veloppeurs du logiciel. Pour autant, la spĂ©cification d’une unitĂ© ne doit pas entrer en contradiction avec la spĂ©cification systĂšme, qui est elle bien basĂ©e sur les besoins du client !

On peut dĂšs lors avoir l’intuition suivante : si chaque unitĂ© se trouve ĂȘtre bien conforme Ă  sa propre petite spĂ©cification − et si de telles petites spĂ©cifications sont cohĂ©rentes avec la spĂ©cifiation globale du systĂšme − alors les chances augmentent que le systĂšme dans son ensemble soit conforme Ă  sa spĂ©cification ! Il en dĂ©coule qu’il apparait trĂšs pertinent de vĂ©rifier chaque unitĂ©, par exemple Ă  l’aide d’une approche de test logiciel qui est alors appelĂ©e test unitaire.

Sur cette base on peut alors distinguer plusieurs niveaux de test, avec au moins :

  • Le test systĂšme, qui consiste Ă  tester le systĂšme dans son ensemble.

  • Le test unitaire, qui consiste Ă  tester chaque unitĂ© sĂ©parĂ©ment, et si possible isolĂ©e des autres.

  • Le test d’intĂ©gration, qui consiste Ă  tester des groupes d’unitĂ©s et leurs interactions.

Le test unitaire est en nature complĂštement identique au test systĂšme (chaque unitĂ© est un "petit systĂšme"), et diffĂšre uniquement en terme d’Ă©chelle.

Si on ajoute au schĂ©ma prĂ©cĂ©dent la notion de scĂ©nario de test, Ă  la fois au niveau systĂšme et au niveau unitaire, on obtient alors :

system vs unit testing.drawio
Figure 5. Test systĂšme, test unitaire
Zoom sur le test unitaire

Le test unitaire est une approche trĂšs populaire et rĂ©pandue, qui a pour particularitĂ© d’ĂȘtre principalement pratiquĂ©e par les dĂ©veloppeurs eux-mĂȘmes pour vĂ©rifier leur code. Elle possĂšde de nombreux avantages, notamment :

  • Facilite la rĂ©utilisation d’une unitĂ©, car la qualitĂ© de chaque unitĂ© aura tendance Ă  augmenter Ă  l’aide des tests unitaires.

  • Facilite la localisation des dĂ©fauts, car si une dĂ©faillance est observĂ©e lors du test d’une unitĂ©, alors il y a de grandes chances que le dĂ©faut soit localisĂ© dans l’unitĂ© en question.

En revanche le test unitaire fait face Ă  des obstacles de testabilitĂ©, notamment :

  • Les oracles doivent pouvoir accĂ©der Ă  l’Ă©tat interne de l’unitĂ©, ce qui peut nĂ©cessitĂ© des accesseurs dĂ©diĂ©s (ex. des getters).

  • Les unitĂ©s qui ne sont pas suffisament accessibles (par exemple avec une visibilitĂ© package en Java) ne sont pas faciles Ă  tester.

La testabilitĂ© fera l’objet d’une partie dĂ©diĂ©e du cours.
Dans ce cours on ne verra que le test unitaire.

3.5. Selon l’approche de conception de scĂ©narios

Enfin, une derniĂšre maniĂšre de catĂ©goriser le test logiciel se repose simplement sur l’approche de test utilisĂ©e pour concevoir des scĂ©narios de test. Par exemple :

  • On parle de test combinatoire lorsqu’on utilise une approche combinatoire pour concevoir des scĂ©narios de test.

  • On parle de test structurel lorsqu’on utilise une approche structurelle pour concevoir des scĂ©narios de test.

  • On parle de test par mutation lorsqu’on utilise une approche pa mutation pour concevoir des scĂ©narios de test.

La suite du cours comportera diffĂ©rentes chapitres pour expliquer ce qu’est une approche de test, et pour prĂ©senter les trois approches sus-mentionnĂ©es.

4. Conclusion

Ce chapitre a présenté toutes les définitions essentielles et générales du test logiciel, le processus général à appliquer pour faire du test logiciel, ainsi que certaines catégorisations existantes du test logiciel. On notera que deux questions importantes subsistent, à savoir comment développer des scripts de test, et comment concevoir des scénario de test.

Pour la suite du cours, on notera que nous nous concentrerons sur du test fonctionnel et unitaire, Ă  la fois en boĂźte noire (approche combinatoire) et en boĂźte blanche (approche structurelle et approche par mutation).