34 votes

Les tests invariants peuvent-ils remplacer les tests unitaires ?

En tant que programmeur, j'ai adhéré de tout cœur à la philosophie TDD et je m'efforce de réaliser des tests unitaires complets pour tout code non trivial que j'écris. Parfois, ce chemin peut être douloureux (les changements de comportement entraînant de multiples changements de tests unitaires en cascade ; de grandes quantités d'échafaudages 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 bogué en conséquence.

Récemment, j'ai joué avec Haskell et sa bibliothèque de tests, QuickCheck. D'une manière très différente de TDD, QuickCheck met l'accent sur le test des invariants du code, c'est-à-dire certaines propriétés qui sont valables pour toutes les entrées (ou des sous-ensembles importants). 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, au moins pour les fonctions pures (c'est-à-dire les fonctions sans effets secondaires - et si vous faites correctement le mocking, vous pouvez convertir des fonctions sales en fonctions pures), que les tests invariants pourraient supplanter les tests unitaires en tant que sur-ensemble strict de ces capacités. Chaque test unitaire consiste en une entrée et 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 concevoir de créer un générateur d'entrées aléatoires suffisamment bon pour couvrir toutes les entrées de tests unitaires que vous auriez créées manuellement (et même plus, car il générerait des cas auxquels vous n'auriez pas pensé) ; si vous trouvez un bogue dans votre programme dû à une condition limite, vous améliorez votre générateur d'entrées aléatoires pour qu'il génère également ce cas.

Le défi consiste donc à savoir s'il est possible ou non de formuler des invariants utiles pour chaque problème. Je dirais que oui : il est beaucoup plus simple, une fois que vous avez une réponse, de voir si elle est correcte que de calculer la réponse en premier lieu. Penser aux invariants permet également de clarifier la spécification d'un algorithme complexe bien mieux que les cas de test ad hoc, qui encouragent une sorte de réflexion au cas par cas du problème. Vous pouvez utiliser une version antérieure de votre programme comme implémentation modèle, ou une version d'un programme dans un autre langage. Etc. Finalement, vous pourriez couvrir tous vos anciens scénarios de test sans avoir à coder explicitement une entrée ou une sortie.

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

24voto

Edward Z. Yang Points 13760

Un an plus tard, je pense maintenant avoir 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 vit 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 aléatoirement. 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, la question de savoir si cela vaut la peine ou non de créer un 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.

9voto

Anthony Points 3706

Ce que vous avez soulevé est un très bon point - lorsqu'il est appliqué uniquement à la programmation fonctionnelle. Vous avez indiqué un moyen d'accomplir tout cela avec du code impératif, mais vous avez également évoqué la raison pour laquelle cela ne se fait pas - 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 s'adapte pas aussi facilement au code impératif.

1voto

Nathan Long Points 30303

Douteux

J'ai seulement entendu parler de ce type de tests (je ne les ai pas utilisés), mais je vois deux problèmes potentiels. J'aimerais avoir des commentaires sur chacun d'eux.

Des résultats trompeurs

J'ai entendu parler de tests comme :

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

Il serait formidable de savoir que cela est vrai pour un large éventail d'entrées. Mais ces deux tests passeraient si les fonctions ne faisaient que retourner leur entrée.

Il me semble que vous voudriez vérifier cela, par exemple, reverse([1 2 3]) est égal à [3 2 1] pour prouver un comportement correct dans au moins un cas, alors ajouter quelques tests avec des données aléatoires.

Complexité des tests

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. Si elle est complexe, elle pourrait être boguée, mais vous n'avez pas de tests pour vos tests.

Un bon test unitaire, en revanche, est trop simple pour qu'un lecteur puisse le rater ou le comprendre de travers. Seule une faute de frappe pourrait créer un bogue dans "attendre reverse([1 2 3]) à égalité [3 2 1] ".

0voto

Ce que vous avez écrit dans votre message original, m'a rappelé ce problème, qui est une question ouverte quant à savoir quel est l'invariant de la boucle pour prouver que la boucle est correcte...

Quoi qu'il en soit, je ne suis pas sûr de ce que vous avez lu en matière de spécifications formelles, mais vous vous dirigez vers 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, c'est dur et compliqué, et cela ne vaut la peine que si vous travaillez sur des systèmes critiques de sécurité. mais je pense qu'il y a des techniques de retour d'enveloppe similaires à ce que quickcheck expose qui peuvent être utilisées.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X