Je ne peux pas pointer vers une bonne ressource en ligne (les articles Wikipédia en anglais sur ces sujets ont tendance à être améliorables), mais je peux résumer une conférence que j'ai entendue et qui couvrait également la théorie des tests de base.
Modes de test
Il existe différentes classes de tests, comme les tests unitaires ou les tests d'intégration . Un test unitaire affirme qu'un morceau de code cohérent (fonction, classe, module) pris sur ses propres travaux comme prévu, tandis qu'un test d'intégration affirme que plusieurs de ces morceaux fonctionnent correctement ensemble.
Un cas de test est un environnement connu dans lequel un morceau de code est exécuté, par exemple en utilisant une entrée de test spécifique ou en se moquant d'autres classes. Le comportement du code est ensuite comparé au comportement attendu, par exemple une valeur de retour spécifique.
Un test ne peut que prouver la présence d'un bug, jamais l'absence de tous les bugs. Les tests mettent une limite supérieure à l'exactitude du programme.
Couverture de code
Pour définir des mesures de couverture de code, le code source peut être traduit en un graphique de flux de contrôle où chaque nœud contient un segment linéaire du code. Le contrôle circule entre ces nœuds uniquement à la fin de chaque bloc, et est toujours conditionnel (si condition, alors passez au nœud A, sinon passez au nœud B). Le graphique a un nœud de début et un nœud de fin.
- Avec ce graphique, la couverture des instructions est le rapport de tous les nœuds visités à tous les nœuds. La couverture complète des relevés n'est pas suffisante pour des tests approfondis.
- La couverture des branches est le rapport de tous les bords visités entre les nœuds du CFG à tous les bords. Cela teste insuffisamment les boucles.
- La couverture de chemin est le rapport de tous les chemins visités à tous les chemins, où un chemin est une séquence d'arêtes du début au nœud de fin. Le problème est qu'avec les boucles, il peut y avoir un nombre infini de chemins, donc la couverture complète des chemins ne peut pas être testée pratiquement.
Il est donc souvent utile de vérifier la couverture des conditions .
- Dans une couverture de condition simple , chaque condition atomique est une fois vraie et une fois fausse - mais cela ne garantit pas une couverture complète des instructions.
- Dans une couverture de conditions multiples , les conditions atomiques ont pris toutes les combinaisons de
true
et false
. Cela implique une couverture complète des succursales, mais est plutôt coûteux. Le programme peut avoir des contraintes supplémentaires qui excluent certaines combinaisons. Cette technique est bonne pour obtenir une couverture de branche, peut trouver du code mort, mais ne peut pas trouver de bogues provenant d'une mauvaise condition.
- Dans la couverture de conditions multiples minimales , chaque condition atomique et composite est une fois vraie et fausse. Cela implique toujours une couverture complète des succursales. Il s'agit d'un sous-ensemble de couverture de conditions multiples, mais nécessite moins de cas de test.
Lors de la construction d'une entrée de test à l'aide d'une couverture de condition, il convient de prendre en compte les courts-circuits. Par exemple,
function foo(A, B) {
if (A && B) x()
else y()
}
doit être testé avec foo(false, whatever)
, foo(true, false)
et foo(true, true)
pour une couverture complète de conditions multiples minimale.
Si vous avez des objets qui peuvent être dans plusieurs états, alors tester toutes les transitions d'état analogues aux flux de contrôle semble judicieux.
Il existe des mesures de couverture plus complexes, mais elles sont généralement similaires aux mesures présentées ici.
Ce sont des méthodes de test en boîte blanche et peuvent être partiellement automatisées. Notez qu'une suite de tests unitaires devrait viser à avoir une couverture de code élevée par n'importe quelle métrique choisie, mais 100% n'est pas toujours possible. Il est particulièrement difficile de tester la gestion des exceptions, où les défauts doivent être injectés dans des emplacements spécifiques.
Tests fonctionnels
Ensuite, il y a des tests fonctionnels qui affirment que le code respecte la spécification en visualisant l'implémentation comme une boîte noire. Ces tests sont utiles pour les tests unitaires et les tests d'intégration. Comme il est impossible de tester avec toutes les données d'entrée possibles (par exemple, tester la longueur de chaîne avec toutes les chaînes possibles), il est utile de regrouper l'entrée (et la sortie) en classes équivalentes - si elle length("foo")
est correcte, elle fonctionnera foo("bar")
probablement également. Pour chaque combinaison possible entre les classes d'équivalence d'entrée et de sortie, au moins une entrée représentative est choisie et testée.
Il faut également tester
- cas de pointe
length("")
, foo("x")
, length(longer_than_INT_MAX)
,
- les valeurs autorisées par la langue, mais pas par le contrat de la fonction
length(null)
, et
- données indésirables possibles
length("null byte in \x00 the middle")
…
En numérique, cela signifie tester 0, ±1, ±x, MAX, MIN, ±∞, NaN
et avec des comparaisons en virgule flottante tester deux flottants voisins. Comme autre ajout, des valeurs de test aléatoires peuvent être choisies dans les classes d'équivalence. Pour faciliter le débogage, il vaut la peine d'enregistrer la graine utilisée…
Tests non fonctionnels: tests de charge, tests de stress
Un logiciel a des exigences non fonctionnelles, qui doivent également être testées. Il s'agit notamment des tests aux limites définies (tests de charge) et au-delà (tests de contrainte). Pour un jeu sur ordinateur, cela pourrait être d'affirmer un nombre minimum d'images par seconde dans un test de charge. Un site Web peut être soumis à des tests de résistance pour observer les temps de réponse lorsque deux fois plus de visiteurs que prévu battent les serveurs. De tels tests ne sont pas seulement pertinents pour des systèmes entiers mais aussi pour des entités individuelles - comment une table de hachage se dégrade-t-elle avec un million d'entrées?
D'autres types de tests sont des tests de système complet dans lesquels des scénarios sont simulés, ou des tests d'acceptation pour prouver que le contrat de développement a été respecté.
Méthodes non testées
Commentaires
Il existe des techniques non testées qui peuvent être utilisées pour l'assurance qualité. Les exemples sont des procédures pas à pas, des révisions de code formelles ou la programmation de paires. Bien que certaines pièces puissent être automatisées (par exemple en utilisant des linters), elles nécessitent généralement beaucoup de temps. Cependant, les révisions de code par des programmeurs expérimentés ont un taux élevé de découverte de bogues et sont particulièrement utiles lors de la conception, où aucun test automatisé n'est possible.
Quand les revues de code sont si bonnes, pourquoi écrivons-nous toujours des tests? Le gros avantage des suites de tests est qu'elles peuvent s'exécuter (la plupart du temps) automatiquement, et sont donc très utiles pour les tests de régression .
Vérification formelle
La vérification formelle va et prouve certaines propriétés du code. La vérification manuelle est surtout viable pour les parties critiques, moins pour les programmes entiers. Les preuves mettent une limite inférieure à l'exactitude du programme. Les épreuves peuvent être automatisées dans une certaine mesure, par exemple via un vérificateur de type statique.
Certains invariants peuvent être vérifiés explicitement à l'aide d' assert
instructions.
Toutes ces techniques ont leur place et sont complémentaires. TDD écrit les tests fonctionnels à l'avance, mais les tests peuvent être jugés par leurs mesures de couverture une fois le code implémenté.
Écrire du code testable signifie écrire de petites unités de code qui peuvent être testées séparément (fonctions d'assistance avec une granularité appropriée, principe de responsabilité unique). Moins chaque fonction prend d'arguments, mieux c'est. Un tel code se prête également à l'insertion d'objets fictifs, par exemple via l'injection de dépendances.
double pihole(double value) { return (value - Math.PI) / (value - Math.PI); }
que j'ai appris de mon professeur de mathématiques . Ce code a exactement un trou , qui ne peut pas être découvert automatiquement à partir des tests de boîte noire seuls. En mathématiques, ce trou n'existe pas. Dans le calcul, vous êtes autorisé à fermer le trou si les limites unilatérales sont égales.