Les tests unitaires sont une méthodologie où les unités de code sont testées de manière isolée du reste de l’application. Un test unitaire peut tester une fonction, un objet, une classe ou un module particulier. Les tests unitaires sont excellents pour apprendre si des parties individuelles d’une application fonctionnent ou non. La NASA a intérêt à savoir si un bouclier thermique fonctionnera ou non avant de lancer la fusée dans l’espace.
Mais les tests unitaires ne permettent pas de vérifier si les unités fonctionnent ensemble lorsqu’elles sont composées pour former une application entière. Pour cela, vous avez besoin de tests d’intégration, qui peuvent être des tests de collaboration entre deux ou plusieurs unités, ou des tests fonctionnels complets de bout en bout de l’ensemble de l’application en cours d’exécution (aka tests système). Finalement, vous devez lancer la fusée et voir ce qui se passe lorsque toutes les parties sont assemblées.
Il existe plusieurs écoles de pensée en matière de tests système, notamment le développement guidé par le comportement (BDD), et les tests fonctionnels.
Le développement guidé par le comportement (BDD) est une branche du développement guidé par les tests (TDD). Le BDD utilise des descriptions lisibles par l’homme des exigences des utilisateurs de logiciels comme base des tests logiciels. Comme la conception pilotée par le domaine (DDD), une des premières étapes du BDD est la définition d’un vocabulaire commun entre les parties prenantes, les experts du domaine et les ingénieurs. Ce processus consiste à définir les entités, les événements et les sorties dont les utilisateurs se soucient, et à leur donner des noms sur lesquels tout le monde peut s’entendre.
Les praticiens du BDD utilisent ensuite ce vocabulaire pour créer un langage spécifique au domaine qu’ils peuvent utiliser pour coder des tests système tels que les tests d’acceptation par l’utilisateur (UAT).
Chaque test est basé sur une histoire d’utilisateur écrite dans le langage ubiquitaire formellement spécifié basé sur l’anglais. (Un langage ubiquitaire est un vocabulaire partagé par toutes les parties prenantes.)
Un test pour un transfert dans un portefeuille de crypto-monnaies pourrait ressembler à ceci:
Story: Transfers change balancesAs a wallet user
In order to send money
I want wallet balances to updateGiven that I have $40 in my balance
And my friend has $10 is their balance
When I transfer $20 to my friend
Then I should have $20 in my balance
And my friend should have $30 in their balance.
Notez que ce langage se concentre exclusivement sur la valeur commerciale qu’un client devrait obtenir du logiciel plutôt que de décrire l’interface utilisateur du logiciel, ou la façon dont le logiciel devrait accomplir les objectifs. C’est le genre de langage que vous pourriez utiliser comme entrée dans le processus de conception UX. Concevoir ces types de besoins des utilisateurs dès le départ peut permettre d’économiser beaucoup de travail plus tard dans le processus en aidant l’équipe et les clients à être sur la même longueur d’onde quant au produit que vous construisez.
À partir de ce stade, vous pouvez vous aventurer sur deux voies :
- Donner au test une signification technique concrète en transformant la description en un langage spécifique au domaine (DSL), de sorte que la description lisible par l’homme se double d’un code lisible par la machine, (continuer sur la voie BDD) ou
- Traduire les user stories en tests automatisés dans un langage généraliste, comme JavaScript, Rust ou Haskell.
Dans tous les cas, c’est généralement une bonne idée de traiter vos tests comme des tests de boîte noire, ce qui signifie que le code de test ne doit pas se soucier des détails d’implémentation de la fonctionnalité que vous testez. Les tests en boîte noire sont moins fragiles que les tests en boîte blanche car, contrairement à ces derniers, les tests en boîte noire ne seront pas couplés aux détails d’implémentation, qui sont susceptibles de changer à mesure que les exigences sont ajoutées ou ajustées, ou que le code est remanié.
Les partisans du BDD utilisent des outils personnalisés tels que Cucumber pour créer et maintenir leurs DSL personnalisés.
Par contraste, les partisans des tests fonctionnels testent généralement les fonctionnalités en simulant les interactions de l’utilisateur avec l’interface et en comparant la sortie réelle à la sortie attendue. Dans les logiciels Web, cela signifie généralement l’utilisation d’un cadre de test qui s’interface avec le navigateur Web pour simuler la frappe, les pressions sur les boutons, le défilement, le zoom, le glissement, etc. et ensuite la sélection de la sortie dans la vue.
Je traduis généralement les exigences des utilisateurs en tests fonctionnels plutôt que de maintenir des tests BDD, principalement en raison de la complexité de l’intégration des cadres BDD avec les applications modernes, et du coût de la maintenance de DSL personnalisés, de type anglais, dont les définitions peuvent finir par couvrir plusieurs systèmes, voire plusieurs langages d’implémentation.
Je trouve le DSL lisible par les profanes utile pour les spécifications de très haut niveau en tant qu’outil de communication entre les parties prenantes, mais un système logiciel typique nécessitera des ordres de grandeur plus importants de tests de bas niveau afin de produire une couverture de code et de cas adéquate pour empêcher les bogues qui mettent en évidence d’atteindre la production.
En pratique, il faut traduire « je transfère 20 $ à mon ami » en quelque chose comme :
- Ouvrir le portefeuille
- Cliquer sur le transfert
- Remplir le montant
- Remplir l’adresse du portefeuille récepteur
- Cliquer.
- Attendre une boîte de dialogue de confirmation
- Cliquer sur « Confirmer la transaction »
Une couche en dessous de ça, vous maintenez l’état pour le flux de travail « transfert d’argent », et vous voudrez des tests unitaires qui garantissent que le bon montant est transféré à la bonne adresse de portefeuille, et une couche en dessous, vous voudrez frapper les API de la blockchain pour vous assurer que les soldes des portefeuilles ont effectivement été ajustés de manière appropriée (quelque chose pour lequel le client peut même ne pas avoir de vue).
Ces différents besoins de test sont mieux servis par différentes couches de tests :
- Les tests unitaires peuvent tester que l’état local du client est mis à jour correctement et présenté correctement dans la vue du client.
- Les tests fonctionnels peuvent tester les interactions de l’interface utilisateur et garantir que les exigences de l’utilisateur sont satisfaites au niveau de la couche de l’interface utilisateur. Cela garantit également que les éléments de l’IU sont câblés de manière appropriée.
- Les tests d’intégration peuvent tester que les communications de l’API se produisent de manière appropriée et que les montants du portefeuille de l’utilisateur ont effectivement été mis à jour correctement sur la blockchain.
Je n’ai jamais rencontré un intervenant profane qui soit vaguement conscient de tous les tests fonctionnels vérifiant même le comportement de l’IU au niveau le plus élevé, et encore moins un qui se soucie de tous les comportements de niveau inférieur. Puisque les profanes ne sont pas intéressés, pourquoi payer le coût de la maintenance d’un DSL pour traduire à leur place ?
Que vous pratiquiez ou non le processus BDD complet, il comporte beaucoup de grandes idées et de pratiques que nous ne devrions pas perdre de vue. Plus précisément :
- La formation d’un vocabulaire partagé que les ingénieurs et les parties prenantes peuvent utiliser pour communiquer efficacement sur les besoins des utilisateurs et les solutions logicielles.
- La création de récits d’utilisateurs et de scénarios qui aident à formuler des critères d’acceptation et une définition de fait pour une fonctionnalité particulière du logiciel.
- La pratique de la collaboration entre les utilisateurs, l’équipe qualité, l’équipe produit et les ingénieurs pour atteindre un consensus sur ce que l’équipe construit.
Une autre approche des tests système est le test fonctionnel.
Qu’est-ce que le test fonctionnel ?
Le terme » test fonctionnel » peut prêter à confusion car il a eu plusieurs significations dans la littérature logicielle.
La norme IEEE 24765 donne deux définitions :
1. Test qui ignore le mécanisme interne d’un système ou d’un composant et se concentre uniquement sur les sorties générées en réponse à des entrées et des conditions d’exécution sélectionnées
2. Les tests effectués pour évaluer la conformité d’un système ou d’un composant à des exigences fonctionnelles spécifiées
La première définition est suffisamment générale pour s’appliquer à presque toutes les formes populaires de tests, et porte déjà un nom parfaitement adapté et bien compris par les testeurs de logiciels : « test de la boîte noire ». Lorsque je parlerai de black box testing, j’utiliserai plutôt ce terme.
La deuxième définition est généralement utilisée par opposition aux tests qui ne sont pas directement liés aux caractéristiques et aux fonctionnalités de l’app, mais qui se concentrent plutôt sur d’autres caractéristiques de l’app, comme les temps de charge, les temps de réponse de l’interface utilisateur, les tests de charge du serveur, les tests de pénétration de la sécurité, etc. Encore une fois, cette définition est trop vague pour être très utile en soi. Habituellement, nous voulons être plus précis sur le type de test que nous faisons, par exemple, des tests unitaires, des tests de fumée, des tests d’acceptation par les utilisateurs…
Pour ces raisons, je préfère une autre définition qui a été populaire récemment. Developer Works d’IBM dit:
Les tests fonctionnels sont écrits du point de vue de l’utilisateur et se concentrent sur le comportement du système qui intéresse les utilisateurs.
C’est beaucoup plus proche de la réalité, mais si nous voulons automatiser les tests, et que ces tests vont tester du point de vue de l’utilisateur, cela signifie que nous devrons écrire des tests qui interagissent avec l’interface utilisateur.
Ces tests peuvent également porter le nom de « tests UI » ou « tests E2E », mais ces noms ne remplacent pas la nécessité du terme « tests fonctionnels », car il existe une classe de tests UI qui testent des choses comme les styles et les couleurs, qui ne sont pas directement liées aux exigences de l’utilisateur comme « je devrais pouvoir transférer de l’argent à mon ami ».
L’utilisation de « tests fonctionnels » pour faire référence aux tests de l’interface utilisateur afin de s’assurer qu’elle répond aux exigences utilisateur spécifiées est généralement utilisée par opposition aux tests unitaires, qui sont définis comme suit :
le test d’unités individuelles de code (telles que des fonctions ou des modules) isolées du reste de l’application
En d’autres termes, alors qu’un test unitaire sert à tester des unités individuelles de code (fonctions, objets, classes, modules) de manière isolée de l’application, un test fonctionnel sert à tester ces unités en intégration avec le reste de l’appli, du point de vue de l’utilisateur qui interagit avec l’interface utilisateur.
J’aime la classification des « tests unitaires » pour les unités de code du point de vue du développeur, et des « tests fonctionnels » pour les tests de l’interface utilisateur du point de vue de l’utilisateur.
Tests unitaires vs tests fonctionnels
Les tests unitaires sont généralement écrits par le programmeur d’implémentation, et testent du point de vue du programmeur.
Les tests fonctionnels sont informés par les critères d’acceptation de l’utilisateur et doivent tester l’application du point de vue de l’utilisateur pour s’assurer que les exigences de l’utilisateur sont satisfaites. Dans de nombreuses équipes, les tests fonctionnels peuvent être écrits ou développés par les ingénieurs qualité, mais chaque ingénieur logiciel doit savoir comment les tests fonctionnels sont écrits pour le projet, et quels tests fonctionnels sont nécessaires pour compléter la « définition de fait » pour un ensemble de fonctionnalités particulier.
Les tests unitaires sont écrits pour tester les unités individuelles de manière isolée du reste du code. Cette approche présente deux avantages majeurs :
- Les tests unitaires s’exécutent très rapidement car ils ne dépendent pas des autres parties du système et, à ce titre, n’ont généralement pas d’E/S asynchrones à attendre. Il est beaucoup plus rapide et moins coûteux de trouver et de corriger une faille avec des tests unitaires que d’attendre l’exécution d’une suite d’intégration complète. Les tests unitaires se terminent généralement en quelques millisecondes, par opposition à des minutes ou des heures.
- Les unités doivent être modulaires pour qu’il soit facile de les tester en les isolant des autres unités. Cela a l’avantage supplémentaire d’être très bon pour l’architecture de l’application. Le code modulaire est plus facile à étendre, à maintenir ou à remplacer, car les effets de sa modification sont généralement limités à l’unité de module testée. Les applications modulaires sont plus flexibles et plus faciles à travailler pour les développeurs au fil du temps.
Les tests fonctionnels, en revanche :
- Ils sont plus longs à exécuter, car ils doivent tester le système de bout en bout, en s’intégrant à toutes les différentes parties et sous-systèmes sur lesquels l’application s’appuie pour permettre le flux de travail de l’utilisateur testé. Les grandes suites d’intégration prennent parfois des heures à exécuter. J’ai entendu des histoires de suites d’intégration qui prenaient plusieurs jours à exécuter. Je recommande d’hyper-optimiser votre pipeline d’intégration pour qu’il s’exécute en parallèle afin qu’il puisse se terminer en moins de 10 minutes – mais c’est encore trop long pour que les développeurs attendent à chaque changement.
- Assurez-vous que les unités fonctionnent ensemble comme un système complet. Même si vous avez une excellente couverture de code des tests unitaires, vous devez toujours tester vos unités intégrées au reste de l’application. Il importe peu que les boucliers thermiques de la NASA fonctionnent s’ils ne restent pas attachés à la fusée lors de la rentrée. Les tests fonctionnels sont une forme de tests système qui garantissent que le système dans son ensemble se comporte comme prévu lorsqu’il est entièrement intégré.
Les tests fonctionnels sans tests unitaires ne peuvent jamais fournir une couverture de code suffisamment profonde pour être sûr que vous avez un filet de sécurité de régression adéquat pour la livraison continue. Les tests unitaires fournissent une profondeur de couverture de code. Les tests fonctionnels fournissent une largeur de couverture des cas de test des exigences de l’utilisateur.
Les tests fonctionnels nous aident à construire le bon produit. (Validation)
Les tests unitaires nous aident à construire le bon produit. (Vérification)
Vous avez besoin des deux.
Note : voir Validation vs Vérification. La distinction construire le bon produit vs construire le bon produit a été succinctement décrite par Barry Boehm.
Comment écrire des tests fonctionnels pour les applications Web
Il existe de nombreux frameworks qui vous permettent de créer des tests fonctionnels pour les applications Web. Beaucoup d’entre eux utilisent une interface appelée Selenium. Selenium est une solution d’automatisation multiplateforme et multi-navigateur créée en 2004 qui vous permet d’automatiser les interactions avec le navigateur web. Le problème avec Selenium est qu’il s’agit d’un moteur externe aux navigateurs qui s’appuie sur Java, et le faire fonctionner avec vos navigateurs peut être plus difficile que nécessaire.
Plus récemment, une nouvelle famille de produits a surgi qui s’intègrent beaucoup plus facilement avec les navigateurs avec moins de pièces à se soucier de l’installation et de la configuration. L’une de ces solutions s’appelle TestCafe. C’est celle que j’utilise et que je recommande actuellement.
Écrivons un test fonctionnel pour le site Web de la journée TDD. Tout d’abord, vous voudrez créer un projet pour celui-ci. Dans un terminal :
mkdir tddday
cd tddday
npm init -y # initialize a package.json
npm install --save-dev testcafe
Maintenant, nous allons devoir ajouter un script "testui"
à notre package.json
dans le bloc scripts
:
{
"scripts": {
"testui": "testcafe chrome src/functional-tests/"
}
// other stuff...
}
Vous pouvez exécuter les tests en tapant npm run testui
, mais il n’y a pas encore de tests à exécuter.
Créer un nouveau fichier à src/functional-tests/index-test.js
:
import { Selector } from 'testcafe';
TestCafe met automatiquement à disposition les fonctions fixture
et test
. Vous pouvez utiliser fixture
avec la syntaxe de littéral de modèle balisé pour créer des titres pour des groupes de tests :
fixture `TDD Day Homepage`
.page('https://tddday.com');
Maintenant vous pouvez sélectionner dans la page et faire des assertions en utilisant les fonctions test
et Select
. Lorsque vous mettez tout cela ensemble, cela ressemble à ceci :
TestCafe va lancer le navigateur Chrome, charger la page, attendre que la page se charge, et attendre que votre sélecteur corresponde à une sélection. S’il ne correspond à rien, le test finira par s’arrêter et échouer. S’il correspond à quelque chose, il vérifiera la valeur réelle sélectionnée par rapport à la valeur attendue, et le test échouera s’ils ne correspondent pas.
TestCafe fournit des méthodes pour tester toutes sortes d’interactions d’interface utilisateur, y compris cliquer, glisser, taper du texte, et ainsi de suite.
TestCafe fournit également une API de sélecteur riche pour rendre les sélections DOM indolores.
TestCafe teste le bouton d’enregistrement pour s’assurer qu’il navigue vers la bonne page lors du clic. Tout d’abord, nous aurons besoin d’un moyen de vérifier l’emplacement actuel de la page. Notre code TestCafe est exécuté dans Node, mais nous avons besoin qu’il soit exécuté dans le client. TestCafe nous fournit un moyen d’exécuter le code dans le client. Tout d’abord, nous devrons ajouter ClientFunction
à notre ligne d’importation :
import { Selector, ClientFunction } from 'testcafe';
Maintenant, nous pouvons l’utiliser pour tester l’emplacement de la fenêtre :
Si vous n’êtes pas sûr de savoir comment faire ce que vous essayez de faire, TestCafe Studio vous permet d’enregistrer et de rejouer des tests. TestCafe Studio est un IDE visuel permettant d’enregistrer et de modifier de manière interactive des tests fonctionnels. Il est conçu pour qu’un ingénieur de test qui ne connaît pas JavaScript puisse créer une suite de tests fonctionnels. Les tests qu’il génère attendent automatiquement les tâches asynchrones telles que les chargements de pages. Comme le moteur TestCafe, TestCafe Studio peut produire des tests qui peuvent être exécutés simultanément sur de nombreux navigateurs, et même sur des appareils distants.
TestCafe Studio est un produit commercial avec un essai gratuit. Vous n’avez pas besoin d’acheter TestCafe studio pour utiliser le moteur TestCafe open source, mais l’éditeur visuel avec des fonctions d’enregistrement intégrées est certainement un outil qui mérite d’être exploré pour voir s’il convient à votre équipe.
TestCafe a placé une nouvelle barre pour les tests fonctionnels multi-navigateurs. Après avoir enduré de nombreuses années à essayer d’automatiser des tests multiplateformes, je suis heureux de dire qu’il existe enfin un moyen assez indolore de créer des tests fonctionnels, et il n’y a maintenant aucune bonne excuse pour négliger vos tests fonctionnels, même si vous n’avez pas d’ingénieurs qualité dédiés pour vous aider à construire votre suite de tests fonctionnels.
Dos et Don’ts des tests fonctionnels
- Ne modifiez pas le DOM. Si vous le faites, votre exécuteur de tests (par exemple, TestCafe) pourrait ne pas être en mesure de comprendre comment le DOM a changé, et cette altération du DOM pourrait avoir un impact sur les autres assertions qui pourraient s’appuyer sur la sortie du DOM.
- Ne partagez pas d’état mutable entre les tests. Parce qu’ils sont si lents, il est incroyablement important que les tests fonctionnels puissent être exécutés en parallèle, et ils ne peuvent pas le faire de manière déterministe s’ils sont en concurrence pour le même état mutable partagé, ce qui pourrait provoquer un non-déterminisme dû à des conditions de course. Parce que vous exécutez des tests système, gardez à l’esprit que si vous modifiez les données utilisateur, vous devriez avoir différentes données utilisateur de test dans la base de données pour différents tests afin qu’ils n’échouent pas aléatoirement en raison de conditions de course.
- Ne mélangez pas les tests fonctionnels avec les tests unitaires. Les tests unitaires et les tests fonctionnels doivent être écrits à partir de perspectives différentes, et exécutés à des moments différents. Les tests unitaires doivent être écrits du point de vue du développeur et s’exécuter chaque fois que le développeur apporte un changement, et doivent se terminer en moins de 3 secondes. Les tests fonctionnels doivent être écrits du point de vue de l’utilisateur et impliquer des entrées/sorties asynchrones, ce qui peut rendre l’exécution des tests trop lente pour permettre au développeur d’avoir un retour immédiat sur chaque modification du code. Il devrait être facile d’exécuter les tests unitaires sans déclencher l’exécution de tests fonctionnels.
- Faites les tests en mode sans tête, si vous le pouvez, ce qui signifie que l’interface utilisateur du navigateur n’a pas réellement besoin d’être lancée, et que les tests peuvent s’exécuter plus rapidement. Le mode sans tête est un excellent moyen d’accélérer la plupart des tests fonctionnels, mais il existe un petit sous-ensemble de tests qui ne peuvent pas être exécutés en mode sans tête, simplement parce que la fonctionnalité sur laquelle ils reposent ne fonctionne pas en mode sans tête. Certains pipelines CI/CD exigeront que vous exécutiez les tests fonctionnels en mode headless, donc si vous avez des tests qui ne peuvent pas être exécutés en mode headless, vous devrez peut-être les exclure de l’exécution CI/CD. Assurez-vous que l’équipe qualité est à l’affût de ce scénario.
- Faites tourner les tests sur plusieurs appareils. Vos tests passent-ils toujours sur les appareils mobiles ? TestCafe peut fonctionner sur des navigateurs distants sans installer TestCafe sur les appareils distants. Cependant, la fonctionnalité de capture d’écran ne fonctionne pas sur les navigateurs distants.
- Faites des captures d’écran lors des échecs de test. Il peut être utile de prendre une capture d’écran si vos tests échouent pour aider à diagnostiquer ce qui n’a pas fonctionné. TestCafe studio a une option de configuration d’exécution pour cela.
- Gardez vos exécutions de tests fonctionnels en dessous de 10 minutes. Tout plus long créera trop de décalage entre le développeur travaillant sur une fonctionnalité et la correction de quelque chose qui a mal tourné. 10 minutes sont suffisamment longues pour qu’un développeur soit occupé à travailler sur la prochaine fonctionnalité, et si le test échoue après plus de 10 minutes, il risque d’interrompre le développeur qui est passé à la tâche suivante. Une tâche interrompue prend en moyenne deux fois plus de temps pour se terminer et contient environ deux fois plus d’erreurs. TestCafe vous permet d’exécuter de nombreux tests simultanément, et l’option de navigateur distant peut le faire sur une flotte de serveurs de test. Je recommande de tirer parti de ces fonctionnalités pour que la durée de vos tests soit aussi courte que possible.
- Arrêter le pipeline de livraison continue lorsque les tests échouent. L’un des grands avantages des tests automatisés est la possibilité de protéger vos clients contre les régressions – des bugs dans des fonctionnalités qui fonctionnaient auparavant. Ce processus de filet de sécurité peut être automatisé afin que vous ayez la certitude que votre version est relativement exempte de bogues. Les tests dans le pipeline CI/CD éliminent efficacement la peur du changement d’une équipe de développement, qui peut être un sérieux frein à la productivité des développeurs.
Next Steps
Jointez TDD Day.com – un cursus TDD d’une journée comprenant 5 heures de contenu vidéo enregistré, des projets pour apprendre les tests unitaires et les tests fonctionnels, comment tester les composants React, et un quiz interactif pour s’assurer que vous avez maîtrisé la matière.