Test unitaire de code fonctionnel typé statiquement

15

Je voulais vous demander aux gens, dans quels cas il est logique de tester un code fonctionnel typé statiquement, comme écrit en haskell, scala, ocaml, nemerle, f # ou haXe (le dernier est ce qui m'intéresse vraiment, mais je voulais puiser dans les connaissances des plus grandes communautés).

Je pose cette question car d'après ma compréhension:

  • Un aspect des tests unitaires est d'avoir les spécifications sous forme exécutable. Cependant, lorsque vous utilisez un style déclaratif, qui mappe directement les spécifications formalisées à la sémantique du langage, est-il même possible d'exprimer les spécifications sous une forme exécutable d'une manière distincte, ce qui ajoute de la valeur?

  • L'aspect le plus évident des tests unitaires est de détecter les erreurs qui ne peuvent pas être révélées par une analyse statique. Étant donné que le code fonctionnel de type sécurisé est un bon outil pour coder extrêmement près de ce que votre analyseur statique comprend, il semblerait que vous puissiez déplacer une grande partie de la sécurité vers l'analyse statique. Cependant, une simple erreur comme l'utilisation xau lieu de y(les deux étant des coordonnées) dans votre code ne peut pas être couverte. OTOH une telle erreur pourrait également survenir lors de l'écriture du code de test, donc je ne suis pas sûr que cela en vaille la peine.

  • Les tests unitaires introduisent une redondance, ce qui signifie que lorsque les exigences changent, le code les implémentant et les tests couvrant ce code doivent tous deux être modifiés. Ce surcoût est bien sûr à peu près constant, on pourrait donc dire que cela n'a pas vraiment d'importance. En fait, dans des langages comme Ruby, cela ne se compare vraiment pas aux avantages, mais étant donné que la programmation fonctionnelle typée statiquement couvre la plupart des tests unitaires au sol, il semble que ce soit une surcharge constante que l'on peut simplement réduire sans pénalité.

J'en déduis que les tests unitaires sont quelque peu obsolètes dans ce style de programmation. Bien sûr, une telle affirmation ne peut conduire qu'à des guerres de religion, alors permettez-moi de résumer cela en une simple question:

Lorsque vous utilisez un tel style de programmation, dans quelle mesure utilisez-vous les tests unitaires et pourquoi (quelle qualité espérez-vous gagner pour votre code)? Ou l'inverse: avez-vous des critères par lesquels vous pouvez qualifier une unité de code fonctionnel typé statiquement comme couvert par l'analyseur statique et donc sans besoin de couverture de test unitaire?

back2dos
la source
4
Soit dit en passant, si vous n'avez pas essayé QuickCheck , vous devriez certainement le faire.
Jon Purdy
scalacheck.org est l'équivalent de Scala
V-Lamp

Réponses:

8

Un aspect des tests unitaires est d'avoir les spécifications sous forme exécutable. Cependant, lorsque vous utilisez un style déclaratif, qui mappe directement les spécifications formalisées à la sémantique du langage, est-il même possible d'exprimer les spécifications sous une forme exécutable d'une manière distincte, ce qui ajoute de la valeur?

Si vous avez des spécifications qui peuvent être directement mappées aux déclarations de fonction, c'est bien. Mais généralement, ce sont deux niveaux d'abstractions complètement différents. Les tests unitaires sont destinés à tester des morceaux de code uniques, écrits sous forme de tests en boîte blanche par le même développeur qui travaille sur la fonction. Les spécifications ressemblent normalement à "lorsque j'entre cette valeur ici et que j'appuie sur ce bouton, ceci et cela devrait se produire". Une telle spécification conduit généralement à plus d'une fonction à développer et à tester.

Cependant, une simple erreur comme l'utilisation de x au lieu de y (les deux étant des coordonnées) dans votre code ne peut pas être couverte. Cependant, une telle erreur pourrait également survenir lors de l'écriture du code de test, donc je ne suis pas sûr que cela en vaille la peine.

Votre idée fausse est ici que les tests unitaires sont en fait pour trouver des bogues dans votre code de première main - ce n'est pas vrai, du moins, ce n'est que partiellement vrai. Ils sont faits pour vous empêcher d'introduire des bogues plus tard lorsque votre code évolue. Donc, lorsque vous avez testé votre fonction pour la première fois et que votre test unitaire a fonctionné (avec "x" et "y" correctement en place), puis pendant la refactorisation, vous utilisez x au lieu de y, le test unitaire vous le montrera.

Les tests unitaires introduisent une redondance, ce qui signifie que lorsque les exigences changent, le code les implémentant et les tests couvrant ce code doivent tous deux être modifiés. Ce surcoût est bien sûr à peu près constant, on pourrait donc affirmer que cela n'a pas vraiment d'importance. En fait, dans des langages comme Ruby, cela ne se compare vraiment pas aux avantages, mais étant donné que la programmation fonctionnelle typée statiquement couvre une grande partie des tests unitaires au sol, il semble que ce soit une surcharge constante que l'on peut simplement réduire sans pénalité.

En ingénierie, la plupart des systèmes de sécurité reposent sur la redondance. Par exemple, deux pauses dans une voiture, un parachute redondant pour un plongeur aérien, etc. La même idée s'applique aux tests unitaires. Bien sûr, avoir plus de code à modifier lorsque les exigences changent peut être un inconvénient. Donc, surtout dans les tests unitaires, il est important de les garder au SEC (suivez le principe "Ne vous répétez pas"). Dans une langue typée statiquement, vous devrez peut-être écrire moins de tests unitaires que dans une langue faiblement typée. En particulier, des tests «formels» peuvent ne pas être nécessaires - ce qui est une bonne chose, car cela vous donne plus de temps pour travailler sur les tests unitaires importants qui testent des choses substantielles. Et ne pensez pas simplement parce que vous avez des types statiques, vous n'avez pas besoin de tests unitaires, il y a encore beaucoup de place pour introduire des bugs lors du refactoring.

Doc Brown
la source
5

Un aspect des tests unitaires est d'avoir les spécifications sous forme exécutable. Cependant, lorsque vous utilisez un style déclaratif, qui mappe directement les spécifications formalisées à la sémantique du langage, est-il même possible d'exprimer les spécifications sous une forme exécutable d'une manière distincte, ce qui ajoute de la valeur?

Il est très peu probable que vous puissiez complètement exprimer vos spécifications sous forme de contraintes de type.

Lorsque vous utilisez un tel style de programmation, dans quelle mesure utilisez-vous les tests unitaires et pourquoi (quelle qualité espérez-vous gagner pour votre code)?

En fait, un avantage majeur de ce style est que les fonctions pures sont plus faciles à tester unitaire: pas besoin de configurer l'état externe ou de le vérifier après l'exécution.

Souvent, la spécification (ou une partie de celle-ci) d'une fonction peut être exprimée sous forme de propriétés reliant la valeur renvoyée aux arguments. Dans ce cas, l'utilisation de QuickCheck (pour Haskell) ou ScalaCheck (pour Scala) peut vous permettre d'écrire ces propriétés en tant qu'expressions de la langue et de vérifier qu'elles sont valides pour les entrées aléatoires.

Alexey Romanov
la source
1
Un peu plus de détails sur QuickCheck: l'idée de base est d'écrire des "propriétés" (invariants dans votre code) et de spécifier comment générer une entrée potentielle. Quickcheck crée ensuite une tonne d'entrées aléatoires et s'assure que votre invariant tient dans tous les cas. Il est plus approfondi que les tests unitaires.
Tikhon Jelvis
1

Vous pouvez considérer les tests unitaires comme un exemple de la façon d'utiliser le code, avec une description de la raison pour laquelle il est précieux.

Voici un exemple, dans lequel l'oncle Bob a eu la gentillesse de s'associer avec moi sur "Game of Life" de John Conway . Je pense que c'est un excellent exercice pour ce genre de chose. La plupart des tests sont du système complet, testant l'ensemble du jeu, mais le premier teste une seule fonction - celle qui calcule les voisins autour d'une cellule. Vous pouvez voir que tous les tests sont écrits sous forme déclarative, avec le comportement que nous recherchons clairement énoncé.

Il est également possible de se moquer des fonctions utilisées dans les fonctions; soit en les passant dans la fonction (l'équivalent de l'injection de dépendance), soit avec des frameworks comme Midje de Brian Marick .

Lunivore
la source
0

Oui, les tests unitaires ont déjà du sens avec un code fonctionnel typé statiquement. Un exemple simple:

prop_encode a = (decode . encode $ a) == a

Vous pouvez forcer prop_encodeavec le type statique.

Zhen
la source