Les tests automatisés seront utilisés dans d'autres tâches et sont également largement utilisés dans des projets réels.
Lorsque nous écrivons une fonction, nous pouvons généralement imaginer ce qu’elle doit faire : quels paramètres donnent quels résultats.
Pendant le développement, nous pouvons vérifier la fonction en l'exécutant et en comparant le résultat avec celui attendu. Par exemple, nous pouvons le faire dans la console.
Si quelque chose ne va pas, nous corrigeons le code, l’exécutons à nouveau, vérifions le résultat – et ainsi de suite jusqu’à ce que cela fonctionne.
Mais ces « réexécutions » manuelles sont imparfaites.
Lorsque vous testez un code par des réexécutions manuelles, il est facile de manquer quelque chose.
Par exemple, nous créons une fonction f
. J'ai écrit du code, testé : f(1)
fonctionne, mais f(2)
ne fonctionne pas. Nous corrigeons le code et maintenant f(2)
fonctionne. Ça a l'air complet ? Mais nous avons oublié de re-tester f(1)
. Cela peut conduire à une erreur.
C'est très typique. Lorsque nous développons quelque chose, nous gardons à l’esprit de nombreux cas d’utilisation possibles. Mais il est difficile d’attendre d’un programmeur qu’il les vérifie tous manuellement après chaque modification. Il devient alors facile de réparer une chose et d’en casser une autre.
Les tests automatisés signifient que les tests sont écrits séparément, en plus du code. Ils exécutent nos fonctions de différentes manières et comparent les résultats avec ceux attendus.
Commençons par une technique nommée Behaviour Driven Development ou, en bref, BDD.
BDD, c'est trois choses en une : des tests ET de la documentation ET des exemples.
Pour comprendre BDD, nous examinerons un cas pratique de développement.
Disons que nous voulons créer une fonction pow(x, n)
qui élève x
à une puissance entière n
. Nous supposons que n≥0
.
Cette tâche n'est qu'un exemple : il existe l'opérateur **
en JavaScript qui peut le faire, mais nous nous concentrons ici sur le flux de développement qui peut également être appliqué à des tâches plus complexes.
Avant de créer le code de pow
, nous pouvons imaginer ce que la fonction doit faire et le décrire.
Une telle description est appelée une spécification ou, en bref, une spécification, et contient des descriptions de cas d'utilisation ainsi que des tests pour ceux-ci, comme ceci :
décrire("pow", fonction() { it("augmente à la puissance n", function() { assert.equal(pow(2, 3), 8); }); });
Une spécification comporte trois éléments de base principaux que vous pouvez voir ci-dessus :
describe("title", function() { ... })
Quelle fonctionnalité décrivons-nous ? Dans notre cas, nous décrivons la fonction pow
. Utilisé pour regrouper les « travailleurs » – it
blocs.
it("use case description", function() { ... })
Dans le titre it
nous décrivons de manière lisible par l'homme le cas d'utilisation particulier, et le deuxième argument est une fonction qui le teste.
assert.equal(value1, value2)
Le code à l'intérieur it
bloc, si l'implémentation est correcte, devrait s'exécuter sans erreur.
Les fonctions assert.*
sont utilisées pour vérifier si pow
fonctionne comme prévu. Ici, nous utilisons l'un d'entre eux – assert.equal
, il compare les arguments et génère une erreur s'ils ne sont pas égaux. Ici, il vérifie que le résultat de pow(2, 3)
est égal à 8
. Il existe d'autres types de comparaisons et de contrôles, que nous ajouterons plus tard.
La spécification peut être exécutée et elle exécutera le test spécifié dans it
bloc. Nous verrons cela plus tard.
Le flux de développement ressemble généralement à ceci :
Une spécification initiale est écrite, avec des tests pour les fonctionnalités les plus élémentaires.
Une première implémentation est créée.
Pour vérifier si cela fonctionne, nous exécutons le framework de test Mocha (plus de détails bientôt) qui exécute la spécification. Bien que la fonctionnalité ne soit pas complète, des erreurs s'affichent. Nous apportons des corrections jusqu'à ce que tout fonctionne.
Nous avons maintenant une implémentation initiale fonctionnelle avec des tests.
Nous ajoutons d'autres cas d'utilisation à la spécification, probablement pas encore pris en charge par les implémentations. Les tests commencent à échouer.
Passez à 3, mettez à jour l'implémentation jusqu'à ce que les tests ne donnent aucune erreur.
Répétez les étapes 3 à 6 jusqu'à ce que la fonctionnalité soit prête.
Le développement est donc itératif . Nous écrivons la spécification, l'implémentons, nous assurons que les tests réussissent, puis écrivons d'autres tests, nous assurons qu'ils fonctionnent, etc. À la fin, nous avons à la fois une implémentation fonctionnelle et des tests pour celle-ci.
Voyons ce flux de développement dans notre cas pratique.
La première étape est déjà terminée : nous avons une première spécification pour pow
. Maintenant, avant de procéder à l'implémentation, utilisons quelques bibliothèques JavaScript pour exécuter les tests, juste pour voir qu'ils fonctionnent (ils échoueront tous).
Ici, dans le didacticiel, nous utiliserons les bibliothèques JavaScript suivantes pour les tests :
Mocha – le framework de base : il fournit des fonctions de test communes, notamment it
describe
et la fonction principale qui exécute les tests.
Chai – la bibliothèque aux nombreuses affirmations. Cela permet d'utiliser beaucoup d'assertions différentes, pour l'instant nous n'avons besoin que assert.equal
.
Sinon – une bibliothèque pour espionner les fonctions, émuler les fonctions intégrées et plus encore, nous en aurons besoin bien plus tard.
Ces bibliothèques conviennent aux tests dans le navigateur et côté serveur. Ici, nous considérerons la variante du navigateur.
La page HTML complète avec ces frameworks et spécifications pow
:
<!DOCTYPEhtml> <html> <tête> <!-- ajoutez du moka css, pour afficher les résultats --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css"> <!-- ajouter le code du framework moka --> <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script> <script> moka.setup('bdd'); // configuration minimale </script> <!-- ajouter du chai --> <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script> <script> // le chai a beaucoup de choses, faisons en sorte qu'il soit global laissez assert = chai.assert; </script> </tête> <corps> <script> fonction pow(x, n) { /* le code de la fonction doit être écrit, vide maintenant */ } </script> <!-- le script avec les tests (décrivez-le...) --> <script src="test.js"></script> <!-- l'élément avec id="mocha" contiendra les résultats des tests --> <div id="moka"></div> <!-- faites des tests ! --> <script> moka.run(); </script> </corps> </html>
La page peut être divisée en cinq parties :
Le <head>
– ajoutez des bibliothèques et des styles tiers pour les tests.
Le <script>
avec la fonction à tester, dans notre cas – avec le code pour pow
.
Les tests – dans notre cas, un script externe test.js
qui a describe("pow", ...)
ci-dessus.
L'élément HTML <div id="mocha">
sera utilisé par Mocha pour afficher les résultats.
Les tests sont lancés par la commande mocha.run()
.
Le résultat :
Pour l'instant, le test échoue, il y a une erreur. C'est logique : nous avons un code de fonction vide dans pow
, donc pow(2,3)
renvoie undefined
au lieu de 8
.
Pour l'avenir, notons qu'il existe davantage de testeurs de haut niveau, comme karma et autres, qui facilitent l'exécution automatique de nombreux tests différents.
Faisons une implémentation simple de pow
, pour que les tests réussissent :
fonction pow(x, n) { retourner 8 ; // :) on triche ! }
Wow, maintenant ça marche !
Ce que nous avons fait est définitivement une triche. La fonction ne fonctionne pas : une tentative de calcul pow(3,4)
donnerait un résultat incorrect, mais les tests réussissent.
…Mais la situation est assez typique, elle se produit dans la pratique. Les tests réussissent, mais la fonction ne fonctionne pas correctement. Notre spécification est imparfaite. Nous devons y ajouter davantage de cas d'utilisation.
Ajoutons un test supplémentaire pour vérifier que pow(3, 4) = 81
.
Nous pouvons sélectionner ici l’une des deux manières d’organiser le test :
La première variante – ajoutez une assert
supplémentaire dans le même it
:
décrire("pow", fonction() { it("augmente à la puissance n", function() { assert.equal(pow(2, 3), 8); assert.equal(pow(3, 4), 81); }); });
La seconde – faites deux tests :
décrire("pow", fonction() { it("2 élevé à la puissance 3 vaut 8", function() { assert.equal(pow(2, 3), 8); }); it("3 élevé à la puissance 4 vaut 81", function() { assert.equal(pow(3, 4), 81); }); });
La principale différence est que lorsque assert
déclenche une erreur, le bloc it
se termine immédiatement. Ainsi, dans la première variante, si la première assert
échoue, nous ne verrons jamais le résultat de la seconde assert
.
Séparer les tests est utile pour obtenir plus d'informations sur ce qui se passe, la deuxième variante est donc meilleure.
Et à part ça, il y a encore une règle qu’il est bon de suivre.
Un test vérifie une chose.
Si nous regardons le test et y voyons deux contrôles indépendants, il est préférable de le diviser en deux contrôles plus simples.
Continuons donc avec la deuxième variante.
Le résultat :
Comme on pouvait s’y attendre, le deuxième test a échoué. Bien sûr, notre fonction renvoie toujours 8
, alors que l' assert
attend 81
.
Écrivons quelque chose de plus réel pour que les tests réussissent :
fonction pow(x, n) { soit le résultat = 1 ; pour (soit i = 0; i < n; i++) { résultat *= x ; } renvoyer le résultat ; }
Pour être sûr que la fonction fonctionne bien, testons-la pour plus de valeurs. Au lieu d' it
des blocs manuellement, nous pouvons les générer for
:
décrire("pow", fonction() { fonction makeTest(x) { soit attendu = x * x * x ; it(`${x} à la puissance 3 est ${expected}`, function() { assert.equal(pow(x, 3), attendu); }); } pour (soit x = 1; x <= 5; x++) { makeTest(x); } });
Le résultat :
Nous allons ajouter encore plus de tests. Mais avant cela, notons que les fonctions d'assistance makeTest
et for
doivent être regroupées. Nous n'aurons pas besoin makeTest
dans d'autres tests, il n'est nécessaire que for
: leur tâche commune est de vérifier comment pow
augmente jusqu'à la puissance donnée.
Le regroupement se fait avec describe
imbriqué :
décrire("pow", fonction() { décrire("élève x à la puissance 3", fonction() { fonction makeTest(x) { soit attendu = x * x * x ; it(`${x} à la puissance 3 est ${expected}`, function() { assert.equal(pow(x, 3), attendu); }); } pour (soit x = 1; x <= 5; x++) { makeTest(x); } }); // ... d'autres tests à suivre ici, les deux décrivent et peuvent être ajoutés });
La describe
imbriquée définit un nouveau « sous-groupe » de tests. Dans le résultat, nous pouvons voir l’indentation intitulée :
À l'avenir, nous pourrons it
ajouter davantage et describe
au niveau supérieur avec leurs propres fonctions d'assistance, ils ne verront pas makeTest
.
before/after
et beforeEach/afterEach
Nous pouvons configurer des fonctions before/after
qui exécutent des tests avant/après, ainsi que des fonctions beforeEach/afterEach
qui s'exécutent avant/après chaque it
.
Par exemple:
décrire("test", fonction() { before(() => alert("Tests démarrés – avant tous les tests")); after(() => alert("Tests terminés – après tous les tests")); beforeEach(() => alert("Avant un test – entrez un test")); afterEach(() => alert("Après un test – quitter un test")); it('test 1', () => alerte(1)); it('test 2', () => alerte(2)); });
La séquence d'exécution sera :
Tests commencés – avant tous les tests (avant) Avant un test – entrez un test (beforeEach) 1 Après un test – quitter un test (afterEach) Avant un test – entrez un test (beforeEach) 2 Après un test – quitter un test (afterEach) Tests terminés – après tous les tests (après)
Ouvrez l'exemple dans le bac à sable.
Habituellement, beforeEach/afterEach
et before/after
sont utilisés pour effectuer l'initialisation, mettre à zéro les compteurs ou faire autre chose entre les tests (ou groupes de tests).
La fonctionnalité de base de pow
est complète. La première itération du développement est terminée. Quand nous aurons fini de célébrer et de boire du champagne, continuons et améliorons-le.
Comme il a été dit, la fonction pow(x, n)
est censée fonctionner avec des valeurs entières positives n
.
Pour indiquer une erreur mathématique, les fonctions JavaScript renvoient généralement NaN
. Faisons de même pour les valeurs invalides de n
.
Ajoutons d'abord le comportement à la spécification (!) :
décrire("pow", fonction() { //... it("pour n négatif, le résultat est NaN", function() { assert.isNaN(pow(2, -1)); }); it("pour n non entier, le résultat est NaN", function() { assert.isNaN(pow(2, 1.5)); }); });
Le résultat avec de nouveaux tests :
Les tests nouvellement ajoutés échouent car notre implémentation ne les prend pas en charge. C'est ainsi que fonctionne BDD : nous écrivons d'abord des tests qui échouent, puis nous réalisons une implémentation pour eux.
Autres affirmations
Veuillez noter l'assertion assert.isNaN
: elle vérifie NaN
.
Il y a également d'autres affirmations dans Chai, par exemple :
assert.equal(value1, value2)
– vérifie l'égalité value1 == value2
.
assert.strictEqual(value1, value2)
– vérifie l'égalité stricte value1 === value2
.
assert.notEqual
, assert.notStrictEqual
– vérifications inverses de celles ci-dessus.
assert.isTrue(value)
– vérifie que value === true
assert.isFalse(value)
– vérifie que value === false
…la liste complète est dans la documentation
Nous devrions donc ajouter quelques lignes à pow
:
fonction pow(x, n) { si (n < 0) renvoie NaN ; if (Math.round(n) != n) renvoie NaN ; soit le résultat = 1 ; pour (soit i = 0; i < n; i++) { résultat *= x ; } renvoyer le résultat ; }
Maintenant ça marche, tous les tests réussissent :
Ouvrez le dernier exemple complet dans le bac à sable.
Dans BDD, la spécification passe en premier, suivie de l'implémentation. À la fin, nous avons à la fois la spécification et le code.
La spécification peut être utilisée de trois manières :
En tant que tests , ils garantissent que le code fonctionne correctement.
Comme Docs – les titres de describe
et it
ce que fait la fonction.
À titre d'exemples : les tests sont en fait des exemples fonctionnels montrant comment une fonction peut être utilisée.
Avec la spécification, nous pouvons améliorer, modifier, voire réécrire la fonction en toute sécurité et nous assurer qu'elle fonctionne toujours correctement.
C'est particulièrement important dans les grands projets lorsqu'une fonction est utilisée à plusieurs endroits. Lorsque nous modifions une telle fonction, il n'y a tout simplement aucun moyen de vérifier manuellement si chaque endroit qui l'utilise fonctionne toujours correctement.
Sans tests, les gens ont deux possibilités :
Pour effectuer le changement, quoi qu’il arrive. Et puis nos utilisateurs rencontrent des bugs, car nous ne parvenons probablement pas à vérifier quelque chose manuellement.
Ou bien, si la punition pour les erreurs est sévère, puisqu'il n'y a pas de tests, les gens ont peur de modifier ces fonctions, et alors le code devient obsolète, personne ne veut y entrer. Pas bon pour le développement.
Les tests automatiques permettent d’éviter ces problèmes !
Si le projet est couvert de tests, ce problème ne se pose tout simplement pas. Après toute modification, nous pouvons exécuter des tests et voir de nombreuses vérifications effectuées en quelques secondes.
De plus, un code bien testé a une meilleure architecture.
Naturellement, cela est dû au fait que le code testé automatiquement est plus facile à modifier et à améliorer. Mais il y a aussi une autre raison.
Pour écrire des tests, le code doit être organisé de telle manière que chaque fonction ait une tâche clairement décrite, des entrées et des sorties bien définies. Cela signifie une bonne architecture dès le début.
Dans la vraie vie, ce n’est parfois pas si simple. Il est parfois difficile d'écrire une spécification avant le code lui-même, car son comportement n'est pas encore clair. Mais en général, l’écriture de tests rend le développement plus rapide et plus stable.
Plus loin dans le didacticiel, vous rencontrerez de nombreuses tâches avec des tests intégrés. Vous verrez donc des exemples plus pratiques.
L'écriture de tests nécessite de bonnes connaissances en JavaScript. Mais nous commençons tout juste à l'apprendre. Donc, pour régler le tout, pour l'instant vous n'êtes pas obligé d'écrire des tests, mais vous devriez déjà pouvoir les lire même s'ils sont un peu plus complexes que dans ce chapitre.
importance : 5
Qu'est-ce qui ne va pas dans le test de pow
ci-dessous ?
it("Augmente x à la puissance n", function() { soit x = 5 ; soit le résultat = x ; assert.equal(pow(x, 1), résultat); résultat *= x ; assert.equal(pow(x, 2), résultat); résultat *= x ; assert.equal(pow(x, 3), résultat); });
PS Syntaxiquement, le test est correct et réussit.
Le test démontre l'une des tentations auxquelles un développeur est confronté lors de l'écriture de tests.
Ce que nous avons ici est en fait 3 tests, mais présentés comme une seule fonction avec 3 assertions.
Parfois, il est plus facile d'écrire de cette façon, mais si une erreur se produit, la cause du problème est beaucoup moins évidente.
Si une erreur se produit au milieu d'un flux d'exécution complexe, nous devrons alors comprendre les données à ce stade. Nous devrons en fait déboguer le test .
Il serait bien préférable de diviser le test en plusieurs blocs it
avec des entrées et des sorties clairement écrites.
Comme ça:
décrire("Augmente x à la puissance n", function() { it("5 à la puissance 1 est égal à 5", function() { assert.equal(pow(5, 1), 5); }); it("5 à la puissance 2 est égal à 25", function() { assert.equal(pow(5, 2), 25); }); it("5 à la puissance 3 est égal à 125", function() { assert.equal(pow(5, 3), 125); }); });
Nous avons remplacé le single it
par describe
et un groupe de blocs it
. Désormais, si quelque chose échoue, nous verrons clairement quelles étaient les données.
Nous pouvons également isoler un seul test et l'exécuter en mode autonome en it.only
écrivant à it
place :
décrire("Augmente x à la puissance n", function() { it("5 à la puissance 1 est égal à 5", function() { assert.equal(pow(5, 1), 5); }); // Mocha exécutera uniquement ce bloc it.only("5 à la puissance 2 est égal à 25", function() { assert.equal(pow(5, 2), 25); }); it("5 à la puissance 3 est égal à 125", function() { assert.equal(pow(5, 3), 125); }); });