Je vais traiter dans cet article des tests logiciels que l’on peut pratiquer au niveau système. Ces tests constituent un maillon essentiel de la chaîne d’intégration continue, ils peuvent être fonctionnels ou non et ont des caractéristiques bien précises. J’aborderai les bonnes pratiques à respecter et les problématiques de mise en œuvre, en me basant sur l’exemple de tests système d’une API. Voyons donc ce que sont les niveaux de tests et ce qu’on appelle un test système.
Les niveaux de test
Conformément aux recommandations de l’ISTQB (International Software Testing Qualification Board), les tests logiciels sont classés en différents niveaux. Voici les niveaux de tests traditionnels, que l’on retrouve dans la classique pyramide des tests :
- Les tests unitaires : ces tests visent à valider chaque unité de code en l’isolant (à l’aide de bouchons, mocks, ou autres fakes…)
- Les tests d’intégration : ces tests s’intéressent aux communications ou échanges de messages entre les différentes unités de code
- Les tests système : ces tests vérifient que les différents aspects d’un système logiciel complet correspondent bien aux spécifications
- Les tests d’acceptation : ces tests visent à obtenir l’acceptation du système dans son ensemble par le client, l’utilisateur ou son représentant
Chez AT Internet, notre solution logicielle étant très étendue et complexe, nous avons choisi d’ajouter deux niveaux de tests :
- Les tests d’intégration de systèmes : ces tests s’intéressent aux échanges entre différents systèmes qui composent notre solution
- Les tests de solution : ces tests valident le bon fonctionnement, dans son ensemble, de notre solution qui est composée de différents systèmes
Le test système
Ces tests « système » constituent le premier niveau des tests dits « boite noire », c’est-à-dire qu’ils sont définis sur la base des spécifications du produit (règles métier, exigences de performance etc…) sans aucun accès au code source. Les tests dits « boite blanche », étant à l’inverse définis sur la base de la structure interne du produit (architecture, code, portions de code, etc…).
Ces tests doivent être totalement indépendants de la structure interne du système (langage et technologies utilisées, détails d’implémentation etc…). Ils doivent se concentrer simplement sur les entrées et sorties du système, en observant ce dernier comme un œil extérieur le ferait. C’est d’ailleurs ce qui fait leur puissance et leur utilité : ils permettent alors de valider un métier ou des comportements directement perceptibles par le client.
Les tests système sont indispensables quand on modifie la structure interne du système (refactoring, remplacement de composants internes, ajout de fonctionnalités…) : ils permettent de s’assurer que ces modifications n’engendrent pas de régressions pour les clients.
En Agilité, nous n’avons pas d’autre option que d’automatiser ces tests, sans quoi l’intégration continue est mise à mal par des délais de livraison qui s’allongent à cause de phases de test manuel parfois interminables. Je ne traiterai donc ici que des tests systèmes automatisés.
Si les tests unitaires et d’intégration sont indispensables et doivent être le plus fournis possible, il est essentiel d’investir sur les tests au niveau système pour s’assurer que le métier souhaité est bien rendu au client.
Chaque brique métier peut en effet correctement faire ce pour quoi elle est conçue mais leur assemblage peut parfois mener à des comportements curieux ! À l’inverse, le métier peut être respecté alors que des parties du système sont parfois en mauvais état, ce qui est tout de même plus rare.
Les tests système sont les premiers tests directement liés aux critères d’acceptation du client dans le processus de test. Ces tests, s’ils sont automatisés, vont permettre de détecter au plus tôt les régressions éventuelles sans quoi on risque de se retrouver avec de mauvaises surprises dans des phases plus tardives du projet : lors de l’intégration du système dans la solution complète ou pire, chez le client ou un de ses représentants (product owner par ex.) .
Il est alors beaucoup plus coûteux de corriger les problèmes détectés, et le risque de retarder la livraison du projet est bien plus important.
Il n’est pas toujours évident de trouver les ressources pour mettre en place et automatiser ce genre de tests : cela nécessite en effet des compétences multiples, tant en conception de tests qu’en développement d’automatismes et nous n’avons pas toujours la chance d’avoir un développeur de tests dans l’équipe. On fera alors parfois appel à des développeurs du produit pour effectuer certaines de ces mises en place.
Le test fait, en effet, partie des activités de développement et la responsabilité de livrer un produit qui fonctionne est bien celle de toute l’équipe, non ?
Quelle approche pour tester un système ?
Pour pouvoir tester correctement un système, il faut commencer par connaître différentes choses à propos de notre système :
Les entrées du système
Il s’agit de tous les déclencheurs d’un comportement du système. On pense souvent aux actions de l’utilisateur qui sont des déclencheurs évidents mais il peut y en avoir de nombreux autres. Voici quelques exemples :
- Une action de l’utilisateur
- La réception de notifications
- Un changement d’état du système
- Le temps qui passe (oui, cela peut déclencher des actions et cela arrive même très souvent : les mécaniques de synchronisation, de planification de tâches etc…).
Les sorties du système
On pense ici souvent à la réponse fournie à l’utilisateur mais il existe d’autres sorties très courantes auxquelles on ne pense pas toujours. Par exemple :
- La réponse à l’utilisateur
- L’écriture de journaux (logs)
- L’écriture de données en base de données
- L’émission d’une notification
Les comportements attendus (ou règles métier)
Nous savons maintenant comment activer le système en jouant sur ses différentes entrées. Nous savons aussi ce que nous pouvons observer en s’intéressant aux différentes sorties. Il nous faut alors connaître les relations entre les entrées et les sorties. Ces relations sont décrites sous forme de règles métier du système et peuvent prendre différentes formes : du critère d’acceptation d’une story utilisateur en Agilité aux spécifications plus détaillées dans d’autres contextes.
Ces comportements, décrits plus ou moins formellement, serviront directement à décrire les différents cas de test qui viendront assurer le rôle de tests système.
Les contextes d’exécution
Certains comportements du système ne dépendent pas seulement des entrées reçues mais aussi du contexte dans lequel le système se trouve au moment du test.
Prenons l’exemple d’un appel à une API qui a pour but d’enregistrer une nouvelle information dans le système. Nous avons alors deux cas, avec différents comportements imaginables :
- L’information est déjà présente dans le système :
- On stocke les deux informations
- On met à jour l’information existante
- On stocke l’historique des valeurs de l’information
- On déclenche une erreur
- …
- L’information n’est pas encore présente
- On enregistre l’information reçue
- On ne fait aucun traitement
- On déclenche une erreur
- …
Dans ces deux situations, le contexte d’exécution a une influence directe dans le comportement attendu du système. C’est à cet endroit que le jeu de données de test prend son importance : on va générer le contexte dans lequel on veut se trouver avant chaque test pour être en mesure de valider les différents comportements de notre système. Nous avons alors un certain nombre de combinaisons de tous ces éléments qui vont constituer nos cas de test :
- Un jeu de données de test
- Une ou plusieurs entrées à « actionner »
- Un comportement attendu
- Une ou plusieurs sorties à contrôler
Les tests système peuvent alors être mis en place. Ceci va nécessiter l’implémentation d’une mécanique de test qui doit être capable de jouer sur les entrées du système et contrôler la validité de son comportement par l’observation de ses différentes sorties :
Suivant les cas, cette mécanique de test est directement mise à disposition dans des outils du marché, en fonction du système que nous souhaitons tester :
- Tests d’API : SOAP UI, supertest, Postman, etc.
- Tests d’interface : Selenium, Cypress, NightwatchJS, etc.
Dans d’autres cas, il va falloir implémenter une mécanique adaptée à vos besoins, qui doit permettre de jouer avec les différentes entrées et sorties du système en test :
- Lecture/écriture dans un topic kafka
- Envoi/récupération de notifications
- Insertion de données dans des bases de données
- Réception de mails
- etc.
On peut constater ici que l’outil de test n’est pas lié à la technologie utilisée dans le système à tester. Il est d’ailleurs bien souvent très différent et n’est pas lié aux besoins de traitement de l’information par le système mais plutôt aux contraintes imposées au(x) client(s) de ce système.
Les aspects non fonctionnels comme la performance ou la résilience peuvent également faire l’objet de tests système, avec différentes techniques et outils variés suivant les besoins.
Tester une API
Prenons l’exemple concret d’une API comme système à tester. Les grands classiques de la validation d’une API sont, dans l’ordre dans lequel le système doit les vérifier :
- Les droits d’utilisation de cette API
- On peut refuser tout de suite un appel non autorisé, quelle que soit sa validité.
- La validation des paramètres
- L’absence de paramètres obligatoires
- La validité de la combinaison de paramètres reçue
- Certains paramètres peuvent parfois être incompatibles et ne doivent donc pas être passés dans un même appel
- Le format de chaque paramètre
- Type, pattern, valeurs autorisées,…
- La cohérence des valeurs reçues pour différents paramètres
- Par exemple recevoir une demande de tri sur un champ non demandé peut parfois être refusé
- La pertinence liée au métier du système
- Il s’agit ici de valider les règles métier du système proprement dites. Par exemple, une demande d’information (GET) sur une propriété inexistante peut déclencher une erreur, l’insertion d’une donnée en base de données via l’API déclenche la réception de son identifiant en retour…
Ces opérations sont de plus en plus coûteuses pour le système qui doit chercher à renvoyer une erreur le plus tôt possible si cela est pertinent, sans enclencher les étapes suivantes, sous peine de générer de la charge inutile sur les serveurs et altérer par là même des aspects non fonctionnels du système, voire même la sécurité de celui-ci.
Pour concevoir ces différents tests, on pourra faire usage de différentes techniques de conception parmi lesquelles l’analyse des valeurs limites, les classes d’équivalence, les transitions d’état, les tables de décisions…
Les tests de performance ou de sécurité peuvent être envisagés avec différents outils comme LOAD UI, Gatling, JMeter, Neoload, par exemple. On retrouve dans ce type de test des techniques bien connues comme :
- Les tests d’injection
- Le fuzz testing
- Le hammering
- La montée en charge progressive
- …
Conclusion
J’espère avoir pu vous éclairer sur ce niveau de test, son importance et comment en tirer le meilleur bénéfice. Il est également important de garder en tête que les tests, leur conception et leur mise en place dépendent toujours de votre contexte et les « bonnes pratiques » doivent être continuellement analysées, remaniées, interprétées pour pouvoir s’appliquer à vos projets de façon optimale.
C’est dans cet état d’esprit que nous investissons, chez AT Internet, dans l’implémentation de tests système automatisés, suivant les besoins de chaque projet et en tenant compte du contexte de chaque équipe. Nous orientons nos investissements conformément à notre stratégie de test avec comme objectif une qualité irréprochable de nos produits au service de nos clients.
Dans le prochain article, j’aborderai 10 pièges à éviter dans le cadre de la mise en place de tests système.
Crédits photo à la une : Markus Spiske
Comments are closed.