Existe-t-il des théories formelles / mathématiques des tests de logiciels?


12

Googler "théorie des tests de logiciels" ne semble donner que des théories au sens doux du terme; Je n'ai pas pu trouver quoi que ce soit qui puisse être qualifié de théorie au sens mathématique, théorique de l'information ou dans un autre domaine scientifique.

Ce que je recherche, c'est quelque chose qui formalise ce qu'est le test, les notions utilisées, ce qu'est un cas de test, la faisabilité de tester quelque chose, la praticité de tester quelque chose, la mesure dans laquelle quelque chose doit être testé, la définition / explication formelle de couverture du code, etc.

MISE À JOUR: De plus, je ne suis pas sûr, intuitivement, du lien entre la vérification formelle et ce que j'ai demandé, mais il y a clairement une sorte de connexion.


1
Les tests de logiciels sont très précieux (par exemple pour mettre en pratique les tests unitaires) mais sa théorie aura toujours quelques trous. Prenons cet exemple classique: 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.
rwong

4
Cela pourrait faire partie de ce que vous recherchez - en.wikipedia.org/wiki/Formal_verification
enderland

1
J'appuie la suggestion de @ enderland. Peu importe la rigueur de votre approche de test; certains bogues passeront toujours à travers les mailles du filet, et à mesure que vous couvrirez plus de code avec vos tests, le coût de la recherche de nouveaux bogues augmente. C'est probablement pourquoi personne n'a eu la peine de formaliser la notion de test - une approche "heuristique" fonctionne presque aussi bien avec moins de formation.
Doval

Je me suis depuis familiarisé avec le pays de la vérification formelle via des types dépendants, et je suis entièrement d'accord avec @Doval et enderland.
Erik Kaplun

1
@rwong Je suppose que vous faites allusion à la possibilité que le numérateur et le dénominateur soient égaux à zéro. Le problème est en partie dû à la mauvaise conception de cette fonction. Lorsque vous parlez de vérification formelle mathématique, vous devez composer vos fonctions non pas arbitrairement mais en suivant des règles formelles, basées sur des types de données corrects. Dans cet exemple, vous devrez utiliser la fonction de division (a,b)=>a/b, qui doit être étendue avec une valeur de dépassement, afin d'être correctement composable.
Dmitri Zaitsev

Réponses:


8

Pour un livre explorant les mathématiques derrière les tests de logiciels ... le livre séminal à obtenir est L'Art de l'analyse des performances des systèmes informatiques: Techniques de conception expérimentale, mesure, simulation et modélisation

Bien qu'il ait été publié pour la première fois en 1991, il est toujours populaire aujourd'hui car c'est un livre de mathématiques appliquées qui se concentre uniquement sur l'analyse des performances, la simulation et les mesures.


5

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 trueet 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, ±∞, NaNet 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' assertinstructions.


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.


2
J'apprécie la réponse élaborée, mais je crains que cela n'ait presque rien à voir avec ce que j'ai demandé :) Mais, heureusement, programmers.stackexchange.com/questions/78675/… semble être aussi proche que possible de ce que j'étais visant à.
Erik Kaplun

C'est génial. Pouvez-vous recommander des livres ou des choses?
Marcin

4

Peut-être que les "tests basés sur les spécifications" répondent également à votre question. Vérifiez ces modules de test (que je n'ai pas encore utilisés). Ils nécessitent que vous écriviez une expression mathématique pour spécifier des ensembles de valeurs de test, plutôt que d'écrire un test unitaire en utilisant des valeurs de données uniques sélectionnées.

Test :: Lectrotest

Comme le dit l'auteur, ce module Perl a été inspiré par le module de vérification rapide de Haskell . Il y a plus de liens sur cette page, dont certains sont morts.


2

Une approche mathématique consiste à tester tous les couples . L'idée est que la plupart des bogues sont activés par un seul choix d'option de configuration, et la plupart des autres sont activés par une certaine paire d'options prises simultanément. Ainsi, la plupart peuvent être capturés en testant "toutes les paires". Une explication mathématique (avec généralisations) est ici:

Le système AETG: une approche des tests basée sur la conception combinatoire

(il existe de nombreuses autres références de ce type)


2

Certaines équations mathématiques sont utilisées, mais cela dépend du type de test logiciel que vous utilisez. Par exemple, l' hypothèse de panne critique suppose que les pannes ne sont guère le produit de deux pannes simultanées ou plus. L'équation suivante est: f = 4n + 1. f = la fonction qui calcule le nombre de cas de test pour un nombre donné de variables ( n) + 1 est l'addition de la constante où toutes les variables prennent la valeur nominale.

Un autre type de test qui nécessite des équations mathématiques est Robustesse Test teste la robustesse ou la correction des cas de test dans un processus de test. Dans ce test, vous saisiriez des variables dans la plage d'entrée légitime (cas de test propres) et des variables d'entrée en dehors de la plage d'entrée (cas de test sales). Vous utiliseriez l'équation mathématique suivante: f = 6n + 1 . 6n indique que chaque variable doit prendre 6 valeurs différentes tandis que les autres valeurs supposent la valeur nominale. * + 1 * représente l'addition de la constante 1.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.