Question

En tant que programmeur, j'ai acheté de tout cœur dans la philosophie TDD et j'ai fait l'effort de faire de vastes tests unitaires pour tout code non trivial que j'écris. Parfois, cette route peut être douloureuse (modifications comportementales provoquant des changements de test unitaire en cascade; des quantités élevées d'échafaudage nécessaires), mais dans l'ensemble, je refuse de programmer sans tests que je peux exécuter après chaque changement, et mon code est beaucoup moins buggy en tant que résultat.

Récemment, j'ai joué avec Haskell, et sa bibliothèque de tests résidents, QuickCheck. D'une manière nettement différente du TDD, QuickCheck met l'accent sur le test des invariants du code, c'est-à-dire certaines propriétés qui retiennent tous les (ou sous-ensembles substantiels) des intrants. Un exemple rapide: un algorithme de tri stable devrait donner la même réponse si nous l'exécutons deux fois, devrait avoir une sortie croissante, devrait être une permutation de l'entrée, etc. Ensuite, QuickCheck génère une variété de données aléatoires afin de tester ces invariants.

Il me semble, du moins pour les fonctions pures (c'est-à-dire des fonctions sans effets secondaires - et si vous vous moquez correctement, vous pouvez convertir des fonctions sales en purs), que les tests invariants pourraient supplanter les tests unitaires comme un sur-ensemble strict de ces capacités . Chaque test unitaire se compose d'une entrée et d'une sortie (dans les langages de programmation impératifs, la "sortie" n'est pas seulement le retour de la fonction mais aussi tout état modifié, mais cela peut être encapsulé). On pourrait éventuellement créer un générateur d'entrée aléatoire qui est assez bon pour couvrir toutes les entrées de test unitaire que vous auriez créé manuellement (puis certains, car cela générerait des cas auxquels vous n'auriez pas pensé); Si vous trouvez un bogue dans votre programme en raison d'une condition aux limites, vous améliorez votre générateur d'entrée aléatoire afin qu'il génère également ce cas.

Le défi est donc de savoir s'il est possible ou non de formuler des invariants utiles pour chaque problème. Je dirais que c'est le cas: c'est beaucoup plus simple une fois que vous avez une réponse pour voir si c'est correct que de calculer la réponse en premier lieu. La réflexion sur les invariants aide également à clarifier la spécification d'un algorithme complexe bien mieux que les cas de test ad hoc, ce qui encourage une sorte de réflexion au cas par cas du problème. Vous pouvez utiliser une version précédente de votre programme comme une implémentation de modèle ou une version d'un programme dans une autre langue. Etc. Finalement, vous pouvez couvrir toutes vos anciennes cas de test sans avoir à coder explicitement une entrée ou une sortie.

Est-ce que je suis devenu fou, ou suis-je sur quelque chose?

Était-ce utile?

La solution

Un an plus tard, je pense maintenant que j'ai une réponse à cette question: Non! En particulier, les tests unitaires seront toujours nécessaires et utiles pour les tests de régression, dans lesquels un test est attaché à un rapport de bogue et se perpétue dans la base de code pour empêcher ce bogue de revenir.

Cependant, je soupçonne que tout test unitaire peut être remplacé par un test dont les entrées sont générées au hasard. Même dans le cas d'un code impératif, «l'entrée» est l'ordre des déclarations impératives que vous devez faire. Bien sûr, qu'il s'agisse ou non de créer le générateur de données aléatoires, et si vous pouvez faire en sorte que le générateur de données aléatoires ait la bonne distribution est une autre question. Les tests unitaires sont simplement un cas dégénéré où le générateur aléatoire donne toujours le même résultat.

Autres conseils

Ce que vous avez évoqué est un très bon point - lorsqu'il est appliqué uniquement à la programmation fonctionnelle. Vous avez déclaré un moyen d'accomplir tout cela avec du code impératif, mais vous avez également expliqué pourquoi ce n'est pas fait - ce n'est pas particulièrement facile.

Je pense que c'est la raison même pour laquelle il ne remplacera pas les tests unitaires: il ne convient pas au code impératif aussi facilement.

Douteux

Je n'ai entendu parler que de (non utilisé) ce type de tests, mais je vois deux problèmes potentiels. J'adorerais avoir des commentaires sur chacun.

Résultats trompeurs

J'ai entendu parler de tests comme:

  • reverse(reverse(list)) devrait être égal list
  • unzip(zip(data)) devrait être égal data

Ce serait formidable de savoir que ceux-ci sont vrais pour un large éventail d'entrées. Mais ces deux tests passeraient si les fonctions renvoient simplement leur entrée.

Il me semble que tu voudrais vérifier cela, par exemple, reverse([1 2 3]) équivaut à [3 2 1] Pour prouver un comportement correct dans au moins un cas, puis ajouter Quelques tests avec des données aléatoires.

Complexité de test

Un test invariant qui décrit entièrement la relation entre l'entrée et la sortie pourrait être plus complexe que la fonction elle-même. S'il est complexe, cela pourrait être buggy, mais vous n'avez pas de tests pour vos tests.

Un bon test unitaire, en revanche, est trop simple pour bousiller ou mal comprendre en tant que lecteur. Seule une faute de frappe pourrait créer un bug dans "attendre reverse([1 2 3]) égaler [3 2 1]".

Ce que vous avez écrit dans votre article d'origine, m'a rappelé ce problème, qui est une question ouverte sur ce que l'invariant de la boucle est de prouver la boucle correcte ...

Quoi qu'il en soit, je ne sais pas combien vous avez lu dans les spécifications formelles, mais vous descendez cette ligne de pensée. Le livre de David Gries est l'un des classiques sur le sujet, je n'ai toujours pas maîtrisé le concept assez bien pour l'utiliser rapidement dans ma programmation quotidienne. La réponse habituelle aux spécifications formelles est, elle est dure et compliquée, et ne vaut l'effort que si vous travaillez sur des systèmes critiques de sécurité. Mais je pense qu'il existe des techniques d'enveloppe à l'arrière similaires à ce que QuickCheck expose qui peut être utilisé.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top