📘 Introduction
1. Contexte
La création d’un logiciel ne part pas de rien : elle vise à répondre à un ensemble donné de besoins, qui peuvent varier grandement en fonction du contexte (acteurs, milieu, contraintes …). Par exemple, un besoin peut être d’écrire du texte, de visionner des vidéos, de réguler la vitesse d’une voiture, de manipuler des listes d’éléments en Java ou de constituer un fichier clientèle.
Quel que soit le processus de développement choisi (cycle en V, cycles de développement courts, etc.), la création d’un logiciel repose sur un ensemble d’activités exercées par les analystes, ingénieurs et développeurs concernés. Schématiquement, on a généralement la situation suivante :
| Ce schéma ne décrit pas un ordre chronologique absolu dans lequel les activités doivent être exercées, mais simplement les relations entre activités d’un point de vue entrées/sorties. On peut ainsi très bien retrouver ces mêmes activités dans un processus de type "cycle en V" — où on passerait une seule fois par chaque activité — ainsi que dans un processus agile — où on repasserait par chacune des activités à chaque itération. |
De gauche à droite :
-
Un ensemble de besoins est à l’origine de la création d’un logiciel.
-
L’activité "Analyse du domaine / Ingénierie des exigences" est menée afin de bien comprendre ces besoins et de les structurer sous la forme d’un ensemble d'exigences, aussi appelé cahier des charges. Ce document est généralement rédigé sous la forme de texte et de modèles (par exemple des cas d’utilisation UML). En outre, cette activité peut définir un ensemble de critères et objectifs de qualité, qui vont contraindre de quelle manière le logiciel doit être conçu et développé (ex. quelles bonnes pratiques à appliquer). On dit souvent que cette activité définit le "quoi".
-
L’activité "Conception" est menée afin de définir tous les aspects du logiciel à réaliser, en produisant un ensemble de documents de conception. Ce travail est mené en se basant sur une analyse rigoureuse du cahier des charges, même lorsque ce dernier est encore en évolution. Les documents de conception définissent toutes les propriétés d’un logiciel capable de répondre à un besoin. Cela inclut notamment l’architecture, les interfaces, les structures de données ou les composants. En outre, la conception doit satisfaire certains critères et objectifs de qualité déjà établis, et va définir de nouveaux critères et objectifs qui devront être pris en compte lors de l’activité de développement (ex. niveau de qualité de code souhaité). On dit souvent que cette activité définit le "comment".
-
Enfin, l’activité "Développement" est menée pour produire un logiciel exécutable, cela en respectant rigoureusement tout ce qui a été prévu par la conception.
Dans ce contexte et dans ce cours, une question centrale que l’on se pose est la suivante : est-ce que le logiciel produit satisfait bel et bien les besoins identifiés au départ ?
2. Spécification d’un logiciel
2.1. Définition
On a vu que le logiciel doit respecter tout le contenu des documents de conception, et que eux-même ont été préparés à partir du cahier des charges. Transitivement, un logiciel devra donc normalement également respecter le dit cahier des charges.
Dans ce cours, on appellera la spécification d’un logiciel tous les documents qui définissent ce qui est attendu de la part d’un logiciel, ce qui inclut donc à la fois le cahier des charges, les documents de conception, et les critères et objectifs de qualité. Une spécification forme en pratique un ensemble de contraintes qui doivent être directement ou indirectement satisfaites par le logiciel construit.
| Dans la littérature, le mot spécification peut parfois être utilisé différemment, par exemple comme un synonyme uniquement de "cahier des charges" ou de "document de conception". Dans ce cours on adopte une définition plus large qui englobe tout. |
2.2. Contenu et forme d’une spécification
Voyons de quoi peuvent être faits les différentes parties d’une spécification.
Le cahier des charges se compose d’un ensemble d’exigences. Une exigence exprime une contrainte que doit satisfaire le logiciel, ou un objectif que l’on doit pouvoir accomplir avec ce dernier. En principe, elle ne doit pas définir comment le logiciel doit être développé pour accomplir une tâche, mais simplement comment satisfaire un besoin. Une exigence peut être exprimée sous de nombreuses formes, notamment :
-
Un langage de modélisation comme UML, SySML ou KAOS peut être utilisé pour produire des définir des exigences sous la forme de modèles. Le diagramme de cas d’utilisation UML est un exemple typique.
-
Une exigence peut prendre la forme d’un récit utilisateur (user story en anglais), à savoir une description textuelle d’un scénario d’usage attendu du logiciel. Un récit utilisateur prend généralement la forme d’une phrase de la forme « En tant que <rôle>, je veux <quoi> afin de <pourquoi> ».
-
Et on peut également définir une exigence de manière plus simple à l’aide de texte ou de schémas explicatifs informels.
On souhaite un logiciel qui nous permette de nous indiquer si un mot est un palindrome.
-
Le logiciel doit nous répondre de manière positive si un mot est un palindrome, et de manière négative sinon.
-
Le logiciel doit avant tout fonctionner sur des mots du dictionnaire, et doit refuser des mots contenant des caractères spéciaux ou des chiffres.
-
Le logiciel doit ignorer la différence entre des lettres minuscules et majuscules.
-
Le logiciel doit être utilisable par quelqu’un qui parle français.
Les documents de conception sont rédigés à partir du cahier des charges, et sont composés de toutes les informations nécessaires pour développer le logiciel. Ils spécifient l’architecture du logiciel, l’ensemble de ses composants et de ses modules, les interfaces de ces derniers, les signatures et spécifications des opérations à implémenter, et même éventuellement certains algorithmes ou processus de certains comportements à développer. On peut également inclure des scénarios d’exécution qui illustrent comment tous les éléments spécifiés s’orchestrent lors de l’exécution. En principe, il suffit de suivre "bêtement" ces documents pour développer le logiciel prévu. Les éléments qui constituent la conception peuvent prendre là aussi de nombreuses formes, notamment :
-
Un langage de modélisation comme UML permet de définir de très nombreux éléments sous la forme de modèles : diagrammes de classe, diagrammes de séquence, diagrammes de composants, diagrammes d’états-transition… la plupart des aspects d’un logiciel peuvent être modélisés avant leur implémentation.
-
Un langage de programmation peut être utilisé dès la conception pour définir les interfaces des modules ou composants directement sous la forme de code. Par exemple si le développement doit se dérouler en Java, il est possible de préparer les interfaces Java qui devront être implémentées par les différentes parties du système.
-
Dans une approche "dirigée par les tests", l’activité de conception peut de manière très anticipée inclure la définition de scénarios de test simples pour le système et ses différentes sous-parties (note: nous ne traiterons pas de ce cas dans le cours et considérons que les scénarios de tests sont élaborés en aval de la conception et du développement).
-
Pour concevoir les interfaces graphiques on peut préparer des images appelées mock-ups qui proposent comment tous les éléments de l’interface doivent être disposés les uns par rapport aux autres.
-
Enfin, les documents de conception peuvent également reposer sur du texte et des schémas explicatifs informels.
-
Le logiciel prend en entrée une chaîne de caractères non-vide ne comprenant ni chiffres, ni caractères spéciaux. Il renvoie un message d’erreur si ce n’est pas le cas.
-
Le logiciel ignore la différence entre des lettres minuscules et majuscules.
-
Le logiciel se lance dans un terminal doit recevoir comme unique argument le mot à analyser.
-
Après exécution, le logiciel renvoie “
Oui” si le mot entré est un palindrome, et renvoie “Non” si ce n’est pas le cas. -
Le code doit être développé uniquement en Java.
-
La logique métier sera fournie par un composant logiciel central qui implémente 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; }
Enfin, les critères et objectifs de qualité viennent préciser de quelle manière on souhaite développer un logiciel, sous la forme de contraintes supplémentaires devant être respectées à la fois durant la conception et durant le développement. Ces contraintes définissent ce qui est souhaité comme niveau de qualité pour le code produit, par exemple en terme de qualité d’architecture (limiter la quantité de couplage entre classes, bonne séparation interfaces/classes, bonne séparation des préoccupations, …) ou de qualité d’écriture du code (nommages, lisibilité, complexité, …). Certains critères et objectifs de qualité peuvent être choisis en même temps que la définition des exigences (notamment ce qui concerne la qualité souhaitée pour la conception), tandis que les critères plutôt liés à l’écriture du code sont plus facilement définis lors de l’activité de conception, dès lors que les choix techniques sont réalisés. On trouve là aussi des éléments de types assez variés, notamment :
-
Des métriques, qui permettent de chiffrer quantitativement certaines propriétés d’un logiciel, et pouvant ainsi servir de critères de qualité objectifs. Il existent une diversité importante de métriques, par exemple pour mesurer la complexité d’un logiciel ou d’un composant (ex. complexité d’une fonction), ou bien pour mesurer le degré de couplage entre composants (ex. interconnexions entre classes). De nombreuses métriques sont couramment utilisées, comme le nombre de lignes de code, la complexité cyclomatique ou le degré de couplage. Pour une métrique et un projet donnés, on peut définir un objectif sous la forme d’un intervalle de valeurs considérées comme acceptables.
-
Des règles de codages devant être suivies de manière systématique lors du développement, afin d’améliorer la structure, la lisibilité et la justesse du code. Un ensemble de règles de codage peut-être propre à un projet, ou bien peut être généralisé à tous les projets d’une organisation, ou bien peut même provenir encore peut provenir d’une source extérieure — par exemple le Google Java Style Guide rendu public par Google.
-
Des normes, standards et principes, qui définissent de bonnes pratiques devant être appliquées lors de la conception et du développement, par exemple en ce qui concerne la maintenabilité (ex. séparation des préoccupations, définition d’APIs stables, granularité des services et composants), la sécurité (ex. principe de moindre privilège, défense en profondeur, minimisation des surfaces d’attaque), ou la fiabilité (ex. redondance, tolérance d’erreur). Là encore, ces documents peuvent être définis pour un projet donné, ou bien peuvent être généralisés à tous les projets d’une organisation, ou bien peuvent provenir de sources extérieures — par exemple en France l’Agence nationale de la sécurité des systèmes d’information (ANSSI) qui met à disposition des pratiques de développement pouvant éviter des failles de sécurité pour les langages C et Rust, ou bien aux États-Unis le Radio Technical Commission for Aeronautics (RTCA) qui définit le standard international DO-178C pour la sûreté de fonctionnement des systèmes logiciels critiques embarqués dans les avions.
| En ce qui concerne les critères et objectifs de qualité, on observera que les contraintes définies sont rarement propres à un projet particulier et peuvent être réutilisées d’un projet à un autre, car elles sont très génériques. Certaines contraintes peuvent néanmoins être propres à un contexte technique particulier — par exemples des principes ou métriques uniquement pertinents pour les langages de programmation orientés objet. |
-
Les identifiants (noms de variables, classes , méthodes …) ne doivent jamais être des abbréviations.
-
Toutes les dépendances entres classes développées au sein du projet doivent passer par des interfaces, autrement dit :
-
Chaque opération exposée par une classe doit être définie dans une interface implémentée par la classe.
-
Une classe peut dépendre d’une interface, mais ne peut pas dépendre directement d’une autre classe.
-
-
Un paramètre/attribut doit être typé avec le type le plus haut possible dans la hierarchie des types disponibles (ex.
ListvoireCollectionplutôt queArrayList).
2.3. Difficultés dans l’élaboration de la spécification
Produire une spécification de qualité est un exercice complexe. Il est malheureusement facile de produire des documents contenant des incohérences, des angles morts, ou bien qui omettent de traiter de certains cas d’usage importants. Il est également possible que les documents de conception proposent un logiciel qui répond mal aux exigences initiales.
Dans ce cours nous ne traiterons pas de ce type de situations, qui relèvent avant tout des activités d’analyse et de conception rappelées au début. On fera donc ici généralement l’hypothèse qu’une spécification donnée est complète et correcte.
2.4. Défauts et défaillances d’un logiciel
Pour s’assurer qu’un logiciel respecte bien sa spécification, mener rigoureusement les activités mentionnées plus haut (analyse, conception, construction) est indispensable. Mais que se passe-t-il quand, malgré cela, des contraintes de la spécification ne sont pas respectées par le logiciel produit ?
On peut désigner de telles situations à l’aide des termes suivants :
-
Un défaut logiciel (en anglais software defect ou software fault) est une partie d’un logiciel qui ne respecte pas la spécification, ou qui entraîne un non-respect de la spécification — par exemple en causant des défaillances. De par la grande variété de forme que peut prendre une spécification, un défaut peut prendre de nombreuses formes, notamment :
-
une fonctionnalité absente,
-
une fonctionnalité non conforme aux attentes,
-
une faille de sécurité,
-
une erreur de style de codage,
-
un objectif non respecté sur une métrique,
-
un non-respect d’un modèle de conception (architecture, interfaces, …).
-
-
Une défaillance logicielle (en anglais software failure) est l’observation, durant l’execution d’un logiciel, d’un comportement qui ne respecte pas sa spécification. Une défaillance est causée par un défaut, et elle est un signe que le logiciel ne fonctionne pas comme prévu. Il en existe de nombreuses formes, notamment :
-
la production d’une donnée non conforme aux attentes,
-
un changement d’état non conforme aux attentes,
-
le plantage ou blocage du logiciel,
-
un comportement erratique / aléatoire.
-
| Une défaillance est toujours causée par une défaut, mais un défaut ne cause pas toujours une défaillance (ex. une règle de codage non respectée n’entraîne pas forcément une défaillance). |
| Ces deux définitions peuvent varier légèrement en fonction de la littérature. |
| Le mot "bug" (ou "bogue") est un terme utilisé très couramment, mais qui ne possède pas de définition suffisamment précise : en fonction du contexte il peut désigner soit un défaut, soit une défaillance. Ce cours tente donc d’éviter d’utiliser ce mot dans la mesure du possible. |
Lorsqu’on exécute le logiciel Palindrome on observe ceci :
$ java -jar palindrome.jar "Erdre"
> Non
$ java -jar palindrome.jar "erdre"
> Oui
La première exécution nous montre que le logiciel n’ignore pas la casse des caractères, contrairement à ce qu’indiquait la spécification. C’est donc une défaillance.
En regardant le code source on découvre ces lignes :
for (int i = 0; i< word.length() / 2; i++ ) {
char f = word.charAt(i);
char l = word.charAt(word.length() - i - 1);
if (f != l) {
return false;
On peut voir que l’opérateur != est utilisé sur les lettres comparées, ce qui ne permet pas d’ignorer la casse.
On a ici mis le doigt sur un premier défaut, à l’origine de cette défaillance.
En outre, on observe que les variables correspondant respectivement à la première lettre analysée et à la seconde lettre analysée sont nommées f (pour first) et l (pour last), ce qui contredit le premier objectif de qualité.
On a donc ici un second défaut, qui lui ne déclenche pas de défaillance.
2.5. Conséquences de la présence de défauts
D’un point de vue quantitatif, la qualité d’un logiciel est par définition inversement proportionnelle à la quantité de défauts qu’il renferme. L’objectif étant de maximiser la qualité, on cherche généralement à tendre vers "zéro défauts" au sein d’un projet logiciel.
D’un point de vue qualitatif, on peut cependant au moins distinguer deux cas :
-
Les défauts ne causant pas de défaillances vont "simplement" impacter la lisibilité, robustesse et maintenabilité du code, et n’impactent normalement pas la satisfaction des besoins initiaux. Ainsi, on peut être tenté de considérer que de tels défauts sont des anomalies "bénignes" ; cependant, s’ils ne sont pas corrigés, ces défauts peuvent s’accumuler et constituer à une dette technique qui va alors nuire aux évolutions du logiciel en développement.
-
Les défauts causant des défaillances sont eux critiques car directement responsables de manquements quant à la satisfaction des besoins initiaux. Les conséquences négatives peuvent alors varier grandement en fonction du contexte :
-
le cahier des charges n’est pas respecté, ce qui peut entraîner des pertes financières et/ou d’image pour l’organisation qui a produit et/ou vendu le logiciel ;
-
dans tous les contextes, les défaillances peuvent provoquer de l’inconfort pour les usagers du logiciel, voire une perte de productivité dans les activités qui reposent sur le logiciel ;
-
dans certains contextes critiques, les défaillances logicielles peuvent avoir des conséquences graves :
-
mise en danger d’usagers (ex : domaine de la santé, des transports, de l’alimentation, du génie civil)
-
gaspillage de ressources (ex : domaine de l’énergie)
-
services fondamentaux non accessibles (ex : services publics en ligne – impôts, aides sociales, services sociaux, …)
-
-
3. Vérification et validation
3.1. Définition
Pour limiter la présence de défauts dans un logiciel, une stratégie consiste à être capable de les détecter et de les identifier. On distingue généralement deux activités visant à cela :
-
La vérification, qui consiste à évaluer si oui ou non un système satisfait sa conception ainsi que les critères et objectifs de qualité considérés.
-
La validation, qui consiste à évaluer si oui ou non un système satisfait le cahier des charges, et donc les besoins des utilisateurs.
Si on place des activités dans le schéma initial, on obtient alors :
| Cette schématisation prend avant tout le point de vue de l’organisation qui développe le logiciel. Cependant il arrive également que certaines phases de vérification ou de validation soient effectués par des organisations extérieures. Par exemple, un audit de sécurité est une forme de vérification centrée sur des problématiques de cybersécurité, et peut être effectué par une organisation extérieure dont c’est le domaine d’expertise. Une telle vérification extérieure peut reposer sur un ensemble de critères et objectifs de qualité supplémentaires — dans l’exemple précédent, on peut imager des pratiques d’architecture ou de codage pouvant limiter certaines failles de cybersécurité — n’ayant pas été anticipés par l’organisation qui développe le logiciel. |
Dans ce cours on distinguera peu les deux activités, même si nous pratiquerons essentiellement la vérification puisque nous prendrons avant tout les documents de conception comme référence.
3.2. Approches existantes
On trouve un grand nombre d’approches pour vérifier et valider des logiciels. Passons en revue différentes approches :
- Model checking
-
Le model checking est une approche qui consiste a automatiquement regarder si tous les comportements possibles d’un logiciel (ou d’un modèle abstrait d’un logiciel) satisfont une spécification écrite sous la forme de propriétés temporelles (écrites en LTL, CTL ou autre). Exemples d’outils : SCADE, Java PathFinder, NuSMV.
- Preuve formelle
-
La preuve formelle est une famille d’approches visant à analyser un logiciel (ou un modèle abstrait d’un logiciel) et à produire des preuves mathématiques que certaines propriétés sont bien satisfaites, par le biais de déductions. Certaines approches permettent même d’effectuer une grande partie des preuves de manière automatisée sans intervention humaine. Exemples d’outils : Isabelle/HOL, Méthode B, KeY, Coq.
- Revue de code
-
Une bonne relecture du code source d’un projet par des développeurs permet de trouver un nombre impressionnant d’erreurs. Cela peut être réalisé de manière systématique et avec des critères de revue précis pour une meilleure efficacité.
- Analyse statique de code
-
L’analyse statique de code vise à lire automatiquement le code source d’un logiciel pour y trouver des erreurs, et cela sans exécuter le programme. La plupart des compilateurs effectuent une forme d’analyse statique avant la compilation (les fameux "warnings" et "errors"). Exemples d’outils : PMD, FindBugs, CheckStyle, de nombreux IDEs (Eclipse, IntelliJ) et compilateurs.
- Test manuel
-
Le test manuel consiste pour le développeur à manuellement exécuter un logiciel sur sa machine pour en observer le fonctionnement, et pour le comparer à la spécification.
- Test automatisé
-
Le test automatisé consiste à écrire des scripts qui vont automatiquement exécuter le logiciel à tester, et automatiquement s’assurer que son comportement est conforme à la spécification. Exemples d’outils : JUnit (et autres frameworks xUnit), Selenium.
Pour mieux comparer toutes ces approches, on peut considérer au moins trois critères utiles :
-
Est-ce que l’approche effectue une analyse exhaustive (tous les comportements possibles du logiciel sont observés) ou bien partielle (une partie seulement des comportements possibles du logiciel sont observés) ?
-
Est-ce que l’approche est statique (ne nécessite pas l’exécution du logiciel) ou bien dynamique (nécessite l’exécution du logiciel) ?
-
Est-ce que l’approche est automatisée (le logiciel est manipulé sans intervention humaine) ou bien manuelle (le logiciel est manipulé par un humain) ?
Et si on résume toutes ces approches selon les trois critères proposés, on obtient le tableau suivant :
| Exhaustif ou partiel | Statique ou dynamique | Automatisée ou manuel | |
|---|---|---|---|
Model checking |
Exhaustif |
Statique |
Automatisée |
Preuve formelle |
Exhaustif |
Statique |
Semi-automatisée |
Revue de code |
Partiel |
Statique |
Manuel |
Analyse statique |
Partiel |
Statique |
Automatisée |
Test manuel |
Partiel |
Dynamique |
Manuel |
Test automatisé |
Partiel |
Dynamique |
Automatisée |
4. Références
-
Paul Ammann, and Jeff Offutt. Introduction to software testing. Cambridge University Press, 2008.
-
IEEE Standard for System, Software, and Hardware Verification and Validation, 2016. https://standards.ieee.org/ieee/1012/5609/