Si chaque chemin dans un programme est testé, est-ce que cela garantit de trouver tous les bogues?
Si non pourquoi pas Comment pouvez-vous passer en revue toutes les combinaisons possibles de flux de programmes sans trouver le problème s'il en existe un?
J'hésite à suggérer que "tous les bugs" peuvent être trouvés, mais c'est peut-être parce que la couverture de chemin n'est pas pratique (car elle est combinatoire) et qu'elle n'est donc jamais expérimentée?
Remarque: cet article fournit un résumé rapide des types de couverture auxquels je pense.
Réponses:
Non
Car même si vous testez tous les chemins possibles , vous ne les avez toujours pas testés avec toutes les valeurs possibles ou toutes les combinaisons de valeurs possibles . Par exemple (pseudocode):
la source
En plus de la réponse de Mason , il y a aussi un autre problème: la couverture ne pas vous dire quel code a été testé, il vous indique quel code a été exécuté .
Imaginez que vous ayez une suite de tests avec une couverture de chemin à 100%. Supprimez maintenant toutes les assertions et exécutez à nouveau la suite de tests. Voilà, la suite de tests a toujours une couverture de chemin de 100%, mais elle ne teste absolument rien.
la source
ON ERROR GOTO
est aussi un chemin, comme C deif(errno)
.Voici un exemple plus simple pour arrondir les choses. Considérez l'algorithme de tri suivant (en Java):
Maintenant, testons:
Maintenant, considérons que (A) cet appel particulier
sort
renvoyant le résultat correct, (B) tous les chemins de code ont été couverts par ce test.Mais, évidemment, le programme ne trie pas réellement.
Il s'ensuit que la couverture de tous les chemins de code n'est pas suffisante pour garantir que le programme ne comporte aucun bogue.
la source
Considérons la
abs
fonction, qui renvoie la valeur absolue d'un nombre. Voici un test (Python, imaginez un framework de test):Cette implémentation est correcte, mais elle n'obtient qu'une couverture de code de 60%:
Cette implémentation est fausse, mais elle obtient une couverture de code à 100%:
la source
def abs(x): if x == -3: return 3 else: return 0
Vous pouvez éventuellement élider laelse: return 0
pièce et obtenir une couverture à 100%, mais la fonction serait essentiellement inutile même si elle réussit le test unitaire.Autre ajout à la réponse de Mason , le comportement d'un programme peut dépendre de l'environnement d'exécution.
Le code suivant contient un Use-After-Free:
Ce code est un comportement non défini. En fonction de la configuration (version | débogage), du système d'exploitation et du compilateur, il génère différents comportements. Non seulement la couverture de chemin ne garantit pas que vous trouverez le fichier UAF, mais votre suite de tests ne couvre généralement pas les divers comportements possibles du fichier UAF qui dépendent de la configuration.
Sur une autre note, même si la couverture de chemin devait garantir la recherche de tous les bogues, il est peu probable que cela puisse être réalisé dans la pratique quel que soit le programme. Considérez le suivant:
Si votre suite de tests peut générer tous les chemins pour cela, alors félicitations, vous êtes un cryptographe.
la source
cryptohash
, il est un peu difficile de dire ce qu'est "suffisamment petit". Peut-être que cela prend deux jours pour terminer sur un supercalculateur. Mais oui, çaint
pourrait être un peushort
.Il ressort clairement des autres réponses que la couverture de code à 100% dans les tests ne signifie pas une exactitude de code à 100%, ni même que tous les bogues détectés par les tests seront détectés (peu importe les bogues qu'aucun test ne pourrait détecter).
Une autre façon de répondre à cette question est une pratique:
Il existe dans le monde réel, et même sur votre propre ordinateur, de nombreux logiciels développés à l’aide d’un ensemble de tests couvrant 100% des cas, mais qui présentent encore des bogues, notamment des bogues que de meilleurs tests permettraient d’identifier.
Une question impliquée est donc:
Les outils de couverture de code aident à identifier les zones que l'on a négligé de tester. Cela peut être correct (le code est manifestement correct même sans test), il peut être impossible à résoudre (pour une raison quelconque, un chemin ne peut pas être atteint), ou bien il peut être l'emplacement d'un grand bogue puant, maintenant ou suite à des modifications futures.
À certains égards, la vérification orthographique est comparable: quelque chose peut "passer" la vérification orthographique et être mal orthographié de manière à correspondre à un mot du dictionnaire. Ou cela peut "échouer" car les mots corrects ne sont pas dans le dictionnaire. Ou cela peut passer et être un non-sens total. La vérification orthographique est un outil qui vous aide à identifier les endroits que vous avez peut-être manqués dans votre relecture, mais comme elle ne peut pas garantir une relecture correcte et complète, la couverture par code ne peut pas garantir un test complet et correct.
Et bien sûr, la mauvaise façon d’utiliser le correcteur orthographique est bien connue: il est préférable d’adopter toutes les suggestions suggérées pour que la situation de la cane s’aggrave s’aggravant par la suite, si elle laisse un prêt.
Avec la couverture de code, il peut être tentant, surtout si vous avez un 98% presque parfait, de remplir des cas pour que les chemins restants soient atteints.
Cela équivaut à redresser avec une correction orthographique corrigée selon laquelle tous les mots sont météo ou noués, il s'agit de tous les mots appropriés. Le résultat est un fouillis de canards.
Cependant, si vous considérez quels tests les chemins non couverts ont vraiment besoin, l'outil de couverture de code aura fait son travail; pas en vous promettant l'exactitude, mais en soulignant une partie du travail à faire.
la source
La couverture de chemin ne peut pas vous dire si toutes les fonctionnalités requises ont été implémentées. Laisser une fonctionnalité est un bogue, mais la couverture de chemin ne la détectera pas.
la source
Une partie du problème est que la couverture à 100% ne garantit que le code fonctionnera correctement après une seule exécution . Certains bugs, tels que les fuites de mémoire, peuvent ne pas être apparents ou causer un problème après une seule exécution, mais au fil du temps, cela causera des problèmes à l'application.
Par exemple, supposons que vous ayez une application qui se connecte à une base de données. Peut-être que, dans une méthode, le programmeur oublie de fermer la connexion à la base de données une fois la requête terminée. Vous pouvez exécuter plusieurs tests avec cette méthode sans trouver d'erreur dans ses fonctionnalités, mais votre serveur de base de données risque de rencontrer un scénario de rupture des connexions disponibles, car cette méthode particulière n'a pas fermé la connexion lorsqu'elle a été effectuée et les connexions ouvertes doivent maintenant timeout.
la source
times_two(x) = x + 2
, cela sera entièrement couvert par la suite de testsassert(times_two(2) == 4)
, mais cela reste évidemment du code buggy! Pas besoin de fuites de mémoire :)Comme déjà dit, la réponse est NON.
Outre ce qui est dit, des bogues apparaissent à différents niveaux et ne peuvent pas être testés avec des tests unitaires. Juste pour en mentionner quelques-uns:
la source
Qu'est-ce que cela signifie pour chaque chemin à tester?
Les autres réponses sont excellentes, mais je veux juste ajouter que la condition "chaque chemin dans un programme est testé" est elle-même vague.
Considérez cette méthode:
Si vous écrivez un test qui confirme
add(1, 2) == 3
, un outil de couverture de code vous indiquera que chaque ligne est exercée. Mais vous n'avez en réalité rien affirmé à propos de l'effet secondaire global ou de la tâche inutile. Ces lignes exécutées, mais n'ont pas vraiment été testées.Le test de mutation aiderait à trouver des problèmes comme celui-ci. Un outil de test de mutation aurait une liste de moyens prédéterminés pour "muter" le code et voir si les tests réussissent toujours. Par exemple:
+=
à-=
. Cette mutation n'entraînera pas d'échec du test. Cela prouverait donc que votre test n'affirme rien de significatif concernant l'effet secondaire global.Essentiellement, les tests de mutation sont un moyen de tester vos tests . Mais comme vous ne testerez jamais la fonction réelle avec tous les jeux d'entrées possibles, vous n'exécuterez jamais toutes les mutations possibles, ce qui est à nouveau limité.
Chaque test que nous pouvons faire est une heuristique pour passer à des programmes sans bug. Rien n'est parfait.
la source
Eh bien ... oui en fait, si chaque chemin "à travers" le programme est testé. Mais cela signifie que chaque chemin possible à travers tout l'espace de tous les états possibles du programme peut avoir, y compris toutes les variables. Même pour un programme très simple compilé statiquement - par exemple, un ancien correcteur de nombres Fortran - ce n’est pas faisable, bien que cela puisse au moins être imaginable: si vous n’avez que deux variables entières, vous avez en gros la possibilité de relier des points entre eux. une grille bidimensionnelle; cela ressemble beaucoup à Travelling Salesman. Pour n telles variables, vous traitez avec un espace n- dimensionnel, de sorte que pour tout programme réel, la tâche est totalement indisponible.
Pire: pour les choses sérieuses, vous n’avez pas seulement un nombre fixe de variables primitives, mais vous créez des variables à la volée dans des appels de fonction, ou vous avez des variables de taille variable ... ou quelque chose du genre, autant que possible dans un langage complet de Turing. Cela donne à l'espace d'états une dimension infinie, brisant tous les espoirs d'une couverture totale, même avec un équipement de test absurdement puissant.
Cela dit ... en réalité, les choses ne sont pas si sombres. Il est possible de prouver que des programmes entiers sont corrects, mais vous devrez renoncer à quelques idées.
Premièrement: il est vivement conseillé de passer à une langue déclarative. Langues Impératif, pour une raison quelconque, ont toujours été de loin le plus populaire, mais la façon dont ils mélangent des algorithmes avec des interactions du monde réel, il est extrêmement difficile de dire même ce que vous entendez par « correct ».
Beaucoup plus facile dans les langages de programmation purement fonctionnels : ceux-ci établissent une distinction claire entre les propriétés réellement intéressantes des fonctions mathématiques et les interactions floues du monde réel sur lesquelles on ne peut vraiment rien dire. Pour les fonctions, il est très facile de spécifier le «comportement correct»: si, pour toutes les entrées possibles (à partir des types d'argument), le résultat souhaité correspondant sort, alors la fonction se comporte correctement.
Maintenant, vous dites que c'est toujours insoluble ... après tout, l'espace de tous les arguments possibles est en général aussi d'une dimension infinie. C'est vrai - bien que pour une seule fonction, même des tests de couverture naïfs vous mènent bien plus loin que vous ne pourriez l'espérer dans un programme impératif! Cependant, il existe un outil puissant incroyable qui change le jeu: la quantification universelle / polymorphisme paramétrique . En gros, cela vous permet d'écrire des fonctions sur des types de données très généraux, avec la garantie que si cela fonctionne pour un exemple simple de données, cela fonctionnera pour toute entrée possible.
Au moins théoriquement. Il n'est pas facile de trouver les bons types qui sont vraiment si généraux que vous pouvez tout à fait le prouver - vous avez généralement besoin d'un langage typé en fonction de la dépendance , ce qui est plutôt difficile à utiliser. Mais écrire dans un style fonctionnel avec un polymorphisme paramétrique augmente déjà énormément votre «niveau de sécurité» - vous ne trouverez pas nécessairement tous les bogues, mais vous devrez les cacher assez bien pour que le compilateur ne les repère pas!
la source