JeuWeb - Crée ton jeu par navigateur
L'utilité du test unitaire - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : L'utilité du test unitaire (/showthread.php?tid=6973)

Pages : 1 2 3 4 5 6


L'utilité du test unitaire - Xenos - 26-08-2013

Bonjour à tous,

creusant encore un peu les "bonnes pratiques" pour la POO, j'en viens à me poser la question de la véritable utilité des tests unitaires. Même après recherche (sur jeuweb et sur google), je ne suis pas convaincu de leur utilité, et même le contraire: je les trouve quasiment malsains.Présentés comme ils le sont, je ne vois pas en quoi ils sont légitimes pour dire "ma classe fait bien ce qu'on attend d'elle": je trouve que la valeur de ces tests est nulle.

A mon sens, l'informatique doit être approchée comme des maths, et non comme du réel. Ok, dans l'industrie réelle, si on veut tester la fiabilité d'une chaîne de production ou d'un poste de travail, on échantillonne le poste en question en prenant un produit (une voiture par exemple), et en le testant. S'il marche, on considère que ok, tous les produits devraient marcher.
Cette approche me semble assez similaire au test unitaire:


25 assertEquals("Le total de la facture est mal calculé",
26 3 * 150 + 50, 0.0001,
27 maFacture.getTotal());

Cela ressemble vachement à de l'échantillonnage. Or l'informatique n'est pas le réel: ce sont des maths. Faire du test type "échantillonnage" pour valider une classe, c'est comme dire "sin(0) = 0 et sin(1) = 0.84 donc sin(x) >= 0 pour tout x".

Je ne comprends donc pas pourquoi il est légitime de se reposer sur le résultat de tests unitaires pour justifier qu'une fonction marche. Par exemple, si ma fonction doit calculer x², je peux faire:


function testSquare()
{
assertEquals("1² ne vaut pas 1!", square(1), 1);
assertEquals("(-1)² ne vaut pas 1!", square(-1), 1);
}

J'ai donc écris mon code de test, et il foire (la fonction square() n'existe pas encore). Je crée la fonction:


function square($x)
{
return 1;
}

Elle passe le test unitaire sans soucis. Pourtant, elle est loin de calculer le carré du nombre passé en entrée (en serait-ce vraiment un en plus? sans typage, ce n'est pas certain) !

Le pire à mon sens, c'est qu'en se reposant sur les tests unitaires, il devient encore plus difficile de débuger le projet: on se dit que testSquare() passe, donc le bug ne vient pas de square().

En termes mathématiques, on ne démontre pas un théorème en disant "il marche pour telle et telle valeur", mais "il marche quelque soit la valeur qui respecte telles conditions": le nombre de tests à faire serait, en pratique, infini.
A la limite, le test unitaire devrait tester tous les entiers pris en charge par l'ordinateur (soit quand même 2^32)... Cela ne me semble franchement pas possible matériellement, question de puissance de calcul. Encore pire si le paramètre d'entrée de la fonction est une chaîne de caractères, ou une chaine binaire.

Alors, quel intérêt au test unitaire, si on ne peut pas se reposer dessus?


RE: L'utilité du test unitaire - niahoo - 26-08-2013

Écrire les tests s'apprend. Si tu testes une fonction avec uniquement des '1' c'est un peu stupide. La programmation ce n'est pas des math, non. ça utilise les math mais on y trouve également de la logique entre autres choses.

Tu ne peux pas prendre n'importe quel programme et prouver sa "correctitude" avec uniquement des math (bien que tu puisses le faire pour certains programmes).

Encore une fois on retrouve ici un défaut le la programmation orientée objet qui couple les méthodes à un état et il est beaucoup plus difficile de tester des fonctions avec des effets de bord. Maintenant, si tu prend des fonctions sans effet de bord au maximum, quelque soit le paradigme, et que tu génères des suites de test non-triviales, tu aura une base solide pour développer.


RE: L'utilité du test unitaire - Xenos - 26-08-2013

Alors, comment testerais-tu que la fonction "square" marche bien? C'est quoi le test "pas stupide" à mettre en place?
Maths (métamaths incluses) ou logique, si les tests unitaires couvrent N cas élémentaires distincts quels qu'ils soient, ils ne peuvent prouver le bon fonctionnement de l'élément testé que si ces N cas recouvrent tous les cas d'entrée de l'objet testé. En d'autres mots, le test unitaire doit tester chaque type d'entrée, puisque s'il ne le fait pas, une méthode qui répond uniquement aux N tests sera validée.

En d'autres mots, restons sur la logique, la méthode des tests unitaire n'est pas suffisante pour montrer le bon fonctionnement de la classe: "test unitaire validé" n'implique pas "classe validée". Elle n'est même pas nécessaire: à partir du seul code source, sans test, on peut prouver le bon ou mauvais fonctionnement de la méthode/fonction/classe.

Alors pourquoi se baser sur le test unitaire pour en déduire que la classe marche (dans l'absolue) ou qu'après modification du code de la classe, celle-ci marche toujours.
Avec ou sans effet de bord, qu'est ce qui rentre dans la catégorie "trivial"? As-tu un exemple de classe + classe de test qui serait pertinent?
Pourquoi ne pas changer d'approche? au lieu de joindre une classe de test (qui teste des cas précis et non tous les cas), on joindrai un "bout de papier", qui prouverait logiquement (ou mathématiquement) que la classe marche. Une preuve qui, somme toute, dit non pas "ma classe de test a mis en avant que square(x)=x² pour x=1, -1, 7, 4.6" mais "l'étude de mon code source me permet de prouver que square(x)=x² pour tout x entier de valeur absolue compris entre +- (2^16)-1"?


RE: L'utilité du test unitaire - niahoo - 26-08-2013

Ici tu testes une fonction triviale. Ce n'est pas à proprement parler une implémentation. C'est juste un alias pour multiplier deux foix le même nombre, cela revient à tester l'opérateur (*). Et comme (*) est fourni par le langage, soit tu lui fais confiance, soit tu le teste.

Mais avec une fonction qui mérite vraiment un test, tu peux prouver que ta fonction est correcte pour tous les cas si un ou plusieurs cas de base sont corrects, et ensuite faire tes tests sur ces cas de base. Inutile de s'amuser à tester tous les entiers un par un.

L'utilité des tests est ensuite de pouvoir les relancer quand tu fais une modif pour t'assurer qu'il n'y a pas eu de régression.


RE: L'utilité du test unitaire - Roworll - 26-08-2013

Et comment tester mathématiquement que la classe 'Inscription' fonctionne correctement, comment vérifier que la méthode player.attack(target) fait ce qu'il faut ?
Si tu fais une modification dans une classe au fin fond de ton système, est-ce que ton bout de papier sera suffisant pour la validation et confirmer que vehicle.destroy() fonctionne toujours aussi bien ?

Les tests unitaires ne valident pas seulement le fonctionnement au niveau mathématique. C'est bien plus évolué que ça.


RE: L'utilité du test unitaire - Sephi-Chan - 26-08-2013

Ils me servent à documenter ce que je veux obtenir avant d'avoir écrit l'implémentation de cette chose.
Ça permet donc de piloter la conception de mon code et de détecter des régressions introduites par un refactoring.


RE: L'utilité du test unitaire - Xenos - 26-08-2013

Bon, j'aurai quand même aimé avoir un exemple plus codé mais soit.
Citation :Mais avec une fonction qui mérite vraiment un test, tu peux prouver que ta fonction est correcte pour tous les cas si un ou plusieurs cas de base sont corrects
D'accord, donc t'as quand même bien besoin d'une preuve, en plus des tests unitaires, qui dit "si tel et tel cas marchent, alors tous les cas marchent". Qu'est ce qui te prouve, en cas de modification, que la régression ne va pas porter sur cette preuve?

Supposons, Roworll, que j'ai un bout de papier (ou autre système un peu plus évolué type documentation expliquant la preuve) qui prouve que, initialement, vehicle.destroy() marche. Si je fais une modification sur vehicle.destroy(), je dois effectivement refaire la preuve, ou m'assurer que la modification n'a pas altéré les hypothèses de la preuve. Si tel est le cas, la preuve est toujours valable et vehicle.destroy() marchera toujours aussi bien.
Si c'est "plus évolué que cela", alors les tests unitaires valident quoi? Ok, si le test unitaire foire, la classe foire, mais l'inverse est faux... Mon but n'est pas de prouver que ma classe foire, mais de prouver qu'elle marche. Prouver que 2+2 ne vaut pas 5 ne prouve pas que cela vaut 4 (encore moins 3).

En un sens, est-ce qu'il n'existerait pas une méthode qui permettrait de faire des tests type "fonctionnel" (aka, basé sur le test de la fonction et non basé sur le test de certaines valeurs)?
Parce qu'au fond, le test unitaire va à l'encontre de l'encapsulation: je veux tester de l'extérieur d'une classe que l'intérieur de la classe marche bien... C'est... paradoxal je trouve.

@Sephi
D'accord, en tant que documentation par l'exemple, pourquoi pas.
Mais pourquoi est-ce que cela permet de détecter les régression? D'accord, cela peut en détecter certaines, mais clairement pas toutes et donc on se retrouve avec un code "à moitié" validé. Pourquoi ce glissement depuis "documentation / pilotage projet" vers "preuve de fonctionnement"?


RE: L'utilité du test unitaire - Sephi-Chan - 26-08-2013

Pour être très concret, voici deux exemples de specs pour Seelies (du composant Match maker, pour être exact).

new_game_search_spec.rb décrit le comporter du daemon quand il reçoit un message de création d'une nouvelle partie (du type : je suis A et je veux inviter B et C à joueur avec moi dans une partie au format 3 contre 3).

dispatcher_spec.rb décrit comment des équipes en recherche d'adversaires doivent être dispatchés en parties (contre d'autres équipes). Ici, il faut voir si les équipes en recherche pour un même format de jeu n'ont pas de joueurs en commun, par exemple.


Oui, ça ne détecte pas tout systématiquement (et souvent on ne décrit pas complètement un comportement), mais c'est un outil. Smile
Ça forme quand même une preuve de fonctionnement d'un comportement donné dans une situation donnée.


RE: L'utilité du test unitaire - niahoo - 26-08-2013

Citation :Qu'est ce qui te prouve, en cas de modification, que la régression ne va pas porter sur cette preuve?

Citation :Si je fais une modification sur vehicle.destroy(), je dois effectivement refaire la preuve

si tu la change tu la refais. Mais comme l'a dit Sephi c'est un outil.

tiens si tu veux du code, nous en étions à tester l'opérateur * Smile tu peux essayer de prouver que mon implémentation est correcte pour les cas de base et que pour tous X et Y , mult Y X == Y + mult Y (X-1) etc.. (pas dur vu que mon implémentation se contente justement de reprendre cette preuve

Code :
-module(mymath).

-export([test/0,mult/2]).

mult(0, _) -> 0;
mult(_, 0) -> 0;
mult(X, Y) when Y > 0 -> X + mult(X, Y-1);
mult(X, Y) when Y < 0 -> 0 - mult(X, 0-Y).



RE: L'utilité du test unitaire - Roworll - 26-08-2013

L'énorme avantage du teste unitaire reste tout de même, dans une application comportant de multiples classes et méthodes, de pouvoir faire un sanity check complet sans passer deux plombes à le faire à la main via l'interface.
Ça ne permet évidemment pas de faire un code 100% bug free mais au moins, tu es sur que les méthodes fonctionnent.

Comme l'a dit niahoo, écrire des tests, ça s'apprend. Dans certaines entreprises, il existe même des postes entièrement dédiés à l'élaboration de procédures de test.
L'exemple que tu donnes en début de thread, est biaisé car les tests ne sont ni pertinents ni complets.
Rien que le fait de proposer la valeur 2 en entrée aurait révélé le problème.
Il faut donc savoir quoi tester et comment le tester.

En POO, le test unitaire ne valide pas que les valeurs mais aussi le comportement dans une situation donnée comme l'a bien souligné Sephi-Chan.

De toute manière, que ce soit à la main ou via une procédure automatique, il est impossible de tester tous les cas in extenso. Les tests unitaires effectuent une partie du travail mais pas tout. Il ne peuvent s'appliquer qu'aux cas que le testeur souhaite vérifier. c'est pour cela qu'il faut les concevoir de manière structurée.