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


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

Attention: tu fais l'amalgame entre implémentation et algorithme !
Oui, l'algorithme est bon, car par récurrence, c'est ok. Mais non, l'implémentation n'est pas correcte car justement elle ne reflète pas l'algorithme mais une restriction de l'algorithme à la finitude de la machine. Quelque soit la machine, tu auras cette limite. Si ton besoin est de traiter une liste infinie (par exemple, une suite mathématique [U(0)=1 & U(n+1)=2*U(n)], ou la liste de tous les entiers), l'algorithme sera adapté, mais le matériel ne le sera pas (exemple: un algorithme peut calculer la valeur de Pi, mais une implémentation ne fera que l'approximer). Autre exemple, si tu as besoin d'utiliser l'algorithme avec la liste des protons de l'univers, tu peux chercher un moment avant de trouver une machine qui sait le faire (~10 puissance 80). Ou compter le nombre total de hash MD5 ou SHA possibles.
L'algorithme est prouvé comme vrai. L'implémentation est prouvée comme vraie dans un certain cadre qui doit faire partie des hypothèses de l'implémentation.

Le dépassement de pile peut être attrapé par le langage, qui lance une exception qu'on doit rattraper. Il doit sinon être géré par le code:
* Soit en indiquant "cette fonction calcule la longueur d'une liste dans la limite de ... éléments" (donc, le problème est pris en charge dans les hypothèses de l'implémentation, pas dans les hypothèses de l'algorithme)
* Soit en ajoutant une mécanique de test de la profondeur du calcul, par exemple, en ajoutant un argument à len, argument qui indique à quel niveau d'imbrication on est actuellement

Le troll involontaire, c'est possible: c'est ce que tu ranges dans la catégorie "hardware mal adapté". C'est aussi la situation habituelle où tu vas te dire "mais flûte, d'où ca vient ce bug?!" ou "ah oui, j'avais pas pensé à ça". Pourquoi utiliser une approche de "je corrige le bug après qu'il soit arrivé"? Pourquoi ne pas avoir une approche inverse: "j'empêche tout bug d'arriver"? C'est typiquement ce que ferait une approche par preuve, qui va dire "je ne peux utiliser len que pour des liste de moins de ... éléments". Je sais d'avance, avant que le bug n'arrive, que je ne respecte pas les hypothèses: donc ça ne sera pas robuste et ça pourra parfaitement boguer (j'ai toujours écris ce mot de travers...). Si ça ne bug pas, c'est pas "que ça marche": c'est juste un coup de bol, un peu comme désallouer un pointer C++ et s'en servir en suite: ça peut marcher (coup de bol), mais c'est loin d'être garanti donc le code n'est pas fiable et il devient instable.

Au contraire: je pense qu'implémenter le test ET sa solution est une mauvaise approche. A l'université, et dans certaines entreprises surement (à l'école aussi d'ailleurs), on utilise la programmation en binôme: l'un code les tests, l'autre code la solution. Si tu code tes tests et ta solution, c'est comme si tu te créais des problèmes que tu es ensuite fier de savoir résoudre... Tu n'auras pas couvert tout le champ des problèmes possible et tu vas en rater des tas (un peu dans le même style que le type qui te dit "j'ai une question bête" qui te coule tout un projet).

Citation :je ne m'engagerai jamais sur du code que je n'ai pas fait
Et tu utilises des frameworks? :roll:


Et oui, économiquement, il y a d'autres facteurs, mais c'est l'idée que j'aimerai faire passer et non l'exemple: si tu considères que "faire des tests, ça coute moins cher même si c'est pas 100% fiable", je te répondrai que ok, c'est moins cher sur le moment, mais sur la durée ce ne sera pas forcément le cas; investir (littéralement) dans l'élaboration d'une preuve de fonctionnement éviteras de créer des tests. Mais bon, ok, sur ce point, je concède ma totale incompétence car je ne sais pas combien "ça" (dev/ingé/stagiaire...) coûte au niveau d'une entreprise. Je sais juste qu'un stagiaire est payé avec des cacahouètes et que les charges patronales françaises sont obscènes.


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

J'ai du mal à imaginer comment tu comptes appliquer ta logique de preuve dans ton code de tous les jours. La théorie est louable mais la pratique me semble juste impossible.

Une méthode Javascript qui ajoute un élément dans le DOM.
Une méthode (PHP ou autre) qui retourne tous les joueurs paginés par 10.
Idem pour celles dont j'ai donné mes specs plus haut.


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

Dire que l'implémentation n'est pas correcte c'est un peu prétentieux. Voici l'implémentation de length dans la librairie standard de haskell que j'ai honteusement pompée :

length :: [a] -> Int
length [] = 0
length (_:l) = 1 + length l

Je pense que ce langage à fait ses preuves en termes de robustesse.

Mai oui tu as tout à fait raison je fais l'amalgame, et comme les développeurs livrent des implémentations et pas des algorithmes, c'est pour ça qu'on fait des tests qui prennent en compte le cadre d'utilisation et non pas des preuves de la correctature des algorithmes. CQFD j'ai envie de dire. Et moi non plus je ne sais pas combien de temps ça pourrait prendre de prouver une appli complète, c'est vraiment d'un autre niveau. Ceci dit ça m'intéresse de plus en plus.

Citation :Au contraire: je pense qu'implémenter le test ET sa solution est une mauvaise approche. A l'université, et dans certaines entreprises surement (à l'école aussi d'ailleurs), on utilise la programmation en binôme: l'un code les tests, l'autre code la solution. Si tu code tes tests et ta solution, c'est comme si tu te créais des problèmes que tu es ensuite fier de savoir résoudre... Tu n'auras pas couvert tout le champ des problèmes possible et tu vas en rater des tas (un peu dans le même style que le type qui te dit "j'ai une question bête" qui te coule tout un projet).

ça se tient Smile Moi je disais çà parce que en réalisant l'implémentation on a rapidement une idée des cas à tester auxquels on ne pense pas dès le début. Comme on l'a vu, les test c'est du code et à ce titre ça se refactore et ça se maintient. On fait des tests, on implémente et là on pense à d'autres cas et on rajoute des tests. Ou bien ça plante et on teste les cas qui buggent|boguent|bug (^^) Et perso j'adore me créer des problèmes et les résoudre en algo ou en jouant à un JV, et en être fier quand c'était chaud

Mais bon ok pour le binôme. Celui qui fait que les tests doit quand même se faire iech.

Pour les frameworks je m'engage à lire la doc et à l'utiliser du mieux que je peux. Je me fais pas virer si je me plante. Je dois choisir aussi un framework ou à leur tour les devs sont censé pondre un code qui marche, et testé. Un quote que j'aime bien des créateurs d'Erlang :

Citation :“The world is concurrent. Things in the world don’t share data. Things communicate with messages. Things fail.”
- Joe Armstrong

Things fails. Prouver des algorithmes triviaux est une perte de temps, je crois qu'il est bon de se concentrer sur les tests des parties les plus sensibles en priorité. Mais prouver théoriquement certains alorithmes, si on en a les compétences et le temps sera toujours une bonne chose. Faudra quand même tester l'implémentation.

(26-08-2013, 10:42 PM)Sephi-Chan a écrit : Une méthode Javascript qui ajoute un élément dans le DOM.
Une méthode (PHP ou autre) qui retourne tous les joueurs paginés par 10.
Idem pour celles dont j'ai donné mes specs plus haut.

Ces fonctions ont des effets de bord, je ne pense pas qu'on puisse prouver la correctitivité des algos inhérents. Je vais demander sur Stack Ov'


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

J'ai pas de recette pour faire en sorte qu'à partir d'une spécification on puisse trouver le code qui marche bien: y'a besoin d'un code source pour prouver que ce code source est valide quelque soit les entrées qu'on voulait lui passer.
Mais si je prends le javascript, par exemple:

function addToDOM(parentNode, childNode)
{
parentNode.appendChild(childNode);
}

Après, faut prouver que ce code marche (pas facile en javascript, surtout sans typage). Je peux considérer que le langage est fiable, aka que appendChild fait bien ce que la documentation me dit.
Elle me dit que parentNode/childNode doit être un element DOM, donc, il faut soit que cette hypothèse reste et figure dans ma documentation de "addToDOM()", soit prendre en compte dans l'implémentation le cas où ces entrées ne sont pas du bon type.
La documentation ne précise pas de limite sur la quantité d'enfants d'un noeud parent (c'est une lacune à mon sens), donc là... C'est un "?" qui va trainer. Soit faut se le rattapper dans le code, soit faut étoffer la documentation originale, soit il faut fixer une limite "arbitraire", et tester tous les cas. Idem pour le nombre total de noeuds dans la page.

/*
* Ajoute childNode à parentNode si les deux sont des DOM Nodes
*/
function addToDOM(parentNode, childNode)
{
parentNode.appendChild(childNode);
}
ou

/*
* ajoute childNode à parentNode si possible, renvoie une exception si non.
*/
function addToDOM(parentNode, childNode)
{
if (!isDOMNode(parentNode))
throw new IsNotNodeException(parentNode);
if (!isDOMNode(childNode))
throw new IsNotNodeException(childNode);

parentNode.appendChild(childNode);
}

j'ai modifié le code, je refait ma preuve: si je suppose que isDOMNode() a déjà été prouvée (elle renvoie "true" si et seulement si je lui passe un DOMNode) alors il me faut encore prouver la validité du throw. S'il est prouvé comme "fiable", alors addToDOM() est fiable.
Oui, c'est clair que c'est assez "lourd", mais je le redis: c'est un investissement car je sais que cette fonction marchera toujours (tant que les hypothèses sur le langage n'auront pas été modifiées en tous cas).

Note que la méthode peut avoir d'autres "entrées" que celles explicites:
Code PHP :
<?php 
class
{
private
$value=0;
public function
increment($x)
{
if (!
is_int($x))
throw new
NotAnIntegerException($x);
$this->value += $x;
}
}

$this->value est à considérer comme une "entrée" de "increment()". Je devrais donc plutôt parler "d'états" que "d'entrées" d'ailleurs.

Je ne sais pas s'il y a des outils pour ce genre de preuve d'ailleurs, il faudra que je cherche.

Pour le binôme, on inverse "testeur" et "implémenteur" pour éviter justement de se faire chier :p

La difficulté réside dans le fait que, si a() utilise b(), on ne peut pas prouver a() sans avoir déjà prouvé b()...


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

Quand tu as écrit addToDom, tu as mis un bout de documentation (plus ou moins formel) que tu acceptais 2 éléments DOM en entrées. Tu n'as même pas été précis : que se passe-t-il si on donne 2 fois le même élément ? Ou si l'élément conteneur est un nœud Text ? Etc.

Penses-tu qu'il est pertinent de tester tout ça dans le corps de la méthode ?
Faut-il gaspiller tant de ressources pour couvrir une utilisation irresponsable d'un outil ?

À mon sens, essayer de faire du code bullet-proof est d'un ennui mortel et sans aucune valeur ajoutée. Je préfère la philosophie d'Erlang, le fameux let it crash. Et quand je code, je fais souvent du fail-fast : le code balance une exécution et bien souvent je la rattrape au plus haut niveau possible pour éviter de présenter une page tout moche à l'utilisateur ou pour éviter que le daemon ne plante (et encore, ça c'est parce que je fais du Ruby, avec de l'Erlang, je laisserait le superviseur relancer le process).


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

(26-08-2013, 06:51 PM)Sephi-Chan a écrit : Moi j'ai arrêté quand j'ai constaté l'obsession pour la preuve mathématiques. :p
Pas mieux. Associer les tests unitaires aux simples problèmes mathématiques, ça n'a pas de sens.
Je me répète mais pour moi, le test unitaire permet de vérifier que, après une modification, quelque soit l'ampleur et la profondeur de du changement, le code à son niveau le plus basique fonctionne toujours de la manière prévue avec des paramètres donnés.
Il faut juste savoir concevoir les bons tests.

(26-08-2013, 11:50 PM)Sephi-Chan a écrit : Je préfère la philosophie d'Erlang, le fameux let it crash.
J'abonde également dans ce sens.
A vouloir faire du code qui ne plante jamais, on se retrouve avec des crétineries sans nom.
Par exemple, lire le contenu d'une table pour vérifier que la valeur de clé unique qu'on va insérer n'existe pas.
C'est absurde et bouffe des ressources pour rien. Mieux vaut laisser le moteur retourner l'erreur et traiter l'exception, c'est moins gourmand.


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

Pour le addToDOM, c'est à appendChild d'avoir géré le cas du noeud texte ou du noeud parent égal au noeud enfant (la doc manque de précision...); là, la doc dit "parent est un élément, enfant est un noeud" sans plus de précision...
Tester tous les cas dans le corps de la fonction est nécessaire si on veut qu'elle prenne tous les cas en compte (via des exceptions si on veut). Imagine qu'on applique le même procédé pour le matériel ou l'OS... Ce serait la misère O.o Si on se dit "bon allez, le disque dur je dis à l'OS qu'il fait 200Go donc l'OS ne va pas demander à écrire sur le 201eme giga"... Le jour où l'OS demande cela, le matériel crame... Le matériel doit forcément prendre ce cas en compte, à moins de prouver que le cas n'arrivera jamais.
C'est aussi issu du problème de l'absence de typage dans javascript.

Ok, l'approche "let it crash" peut marcher, mais je pense que le problem du "prove it" est l'absence d'outil automatisé. C'est comme se dire "les tests à la main, c'est nul ça bouffe du temps et faut recommencer à chaque fois", donc on est passé à des tests automatiques. A mon avis, le problème de "la preuve c'est nul ça bouffe du temps et c'est compliqué" répond au même besoin d'automatisation: si j'ai un analyseur de code (prouvé à la main :p) qui me permet de prouver que d'autres codes marchent, alors j'aurai des projets robustes, prouvés automatiquement (tiens, je vais m'essayer à ce genre de projet, cela me semble rentable à ce niveau là!).

(Roworll, si tu parles de tables type SQL, c'est déjà ce que le moteur de BDD fait: il lit toute la table, en optimisant avec un arbre si besoin, jusqu'à temps de trouver l'emplacement de la clef et si elle existe déjà, il remonte l'erreur. Faut pas considérer qu'une fonction "prouvée" doit absolument tout vérifier par elle-même: elle peut très bien s'appuyer sur d'autres fonctions prouvées).

Normalement, les bons langages ont justement des preuves formelles de fonctionnement, et je trouve cela dommage de "perdre" cette preuve entre le langage et le projet qui l'utilise.


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

(27-08-2013, 09:33 AM)Xenos a écrit : (Roworll, si tu parles de tables type SQL, c'est déjà ce que le moteur de BDD fait: il lit toute la table, en optimisant avec un arbre si besoin, jusqu'à temps de trouver l'emplacement de la clef et si elle existe déjà, il remonte l'erreur. Faut pas considérer qu'une fonction "prouvée" doit absolument tout vérifier par elle-même: elle peut très bien s'appuyer sur d'autres fonctions prouvées).

J'ai parlé de "crétinerie sans nom" dans mon exemple sur le SQL
Je m'autocite : "lire le contenu d'une table pour vérifier que la valeur de clé unique qu'on va insérer n'existe pas."

La crétinerie en question étant de faire manuellement un SELECT sur la table pour savoir si la clef y est déjà avant d'éventuellement faire un INSERT. Comme tu l'expliques lors de l'INSERT, SQL va justement faire cette opération.
Donc, vérifier avant d'insérer est idiot, c'est juste une perte de temps et de puissance mue par la volonté de faire un code bullet-proof.
Gérer l'erreur à la manière "let it crash" est dans ce cas bien plus naturel, économe et tout aussi efficace.


RE: L'utilité du test unitaire - BAK - 27-08-2013

Il est facile de voir ce que les preuves apportent, et dans un monde idéal, tout les codes seraient prouvés, et les bugs n'existeraient pas.
Mais dans la réalité, les preuves ont un coût important.

On a déjà évoqué la difficulté de trouver des personnes capable de prouver le code, mais ce n'est pas le seul coût, loin de là: poser une preuve, c'est long.
Le but des tests (unitaires ou non), c'est de réduire le nombre de bug, et ainsi gagner du temps.
On va passer 1h à faire des tests, pour économiser 3 ou 4h de debug, c'est rentable. S'il faut 10h pour faire la preuve formelle, alors c'est une perte de temps, aussi bien pour un pro qu'un amateur.

Comme tu l'as dit, si a() utilise b(), il faut prouver b() pour prouver a(). De ce fait, pour être sur à 100% de la perfection de son programme, il faudrait que le langage sont prouvé, ainsi que toutes ses dépendances, et donc l'OS (et je ne parle même pas du hardware, ce qui est impossible). La perfection n'existe pas.

Concernant ton code de la méthode addToDOM, la méthode appendChild gère elle-même les cas particuliers que tu propose en lançant des exceptions.
Rajouter des contrôles à tout les niveaux n'est pas une bonne idée: certes tu peut voir les erreurs plus vite, et être sûr du résultat, mais tu perd énormément en lisibilité et maintenabilité.
Tu as multiplié ton nombre de lignes par 5 ! Tu perd également en performance, le temps de vérification n'est jamais nul.
Enfin, dans ce cas, c'est les specs de appenChild qui sont mal faites: il n'est pas noté qu'elle puisse lancer une exception NOT_FOUND_ERR si l'entrée n'est pas un élément du DOM (alors que c'est le cas pour toutes les autres méthodes travaillant avec des objets du DOM. Chromium le fait).
Pour le check du parent, l'appel à une méthode qui n'existe pas lancera aussi une exception, donc c'est pas utile.


Bien sur, je ne dit pas que les preuves ne servent à rien, au contraire. Et j'en voit parfaitement l'utilité dans les systèmes critiques (systèmes embarqué pour du médical ou de l'aéronautique par exemple).


Sinon, mis à part cette comparaison test / preuve, il y a d'autres intérêt à utiliser les tests unitaires.
Lorsque j'ai commencé à faire des tests la première fois, j'ai écrit le code d'une classe et testé ensuite.
Quand me me suis mis à tester, j'ai vu plein de détails qui me gênaient dans mon code:
- "Tient, pourquoi je doit coder 4 lignes pour faire ce truc qui va être utilisé de partout, alors qu'avec une autre implémentation je pourrais le faire en 2 ?"
- "Bah alors, utiliser la fonction checkMachin à chaque fois, c'est lourd, ce serait plus simple si la classe lançait une exception toute seule"

L'architecture n'était pas mauvaise au contraire. J'ai bien pensé mes différentes classes et leurs interactions, j'ai un diagramme UML complet et solide.
Ce qui me manquait, c'était "l'ergonomie" du code, la vision qu'aurait l'utilisateur de l'API.
Depuis, je commence toujours pas faire les tests avant de coder.


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

Citation :Donc, vérifier avant d'insérer est idiot, c'est juste une perte de temps et de puissance mue par la volonté de faire un code bullet-proof.
Oui, c'est idiot si c'est déjà fait par le SQL, on est d'accord Wink
Ce qui me gène, c'est que les "bornes" de validité d'une fonction sont exagérées dans les docs et il me semble que c'st cela qui rend les codes long à débuger.
La preuve amène un débugage rapide: si la fonction prouvée foire, alors c'est qu'une des hypothèses de base foire (par exemple, l'hypothèse "le hardware est 100% fiable" ou "le SQL gère l'insertion d'une clef déjà existante"). Cela évite d'aller chercher dans le code d'où vient l'erreur: on sait qu'elle vient d'une des hypothèses.

Pour appendChild, faute de doc, je ne savais pas sa réaction, d'où la nécessité de ces lignes de "if". Mais oui, je pense que le problème porte aussi sur l'aspect non formel des documentations.

D'accord pour cet autre aspect du test. On teste plus l'expérience utilisateur de son code que le code lui même dans ce cas, c'est cela?

A mon sens, oui, si je devais classer du moins bon au meilleur, je dirai:
  • Ne pas tester (ne pas savoir si le projet marche)
  • Tester à la main (débug long)
  • Prouver à la main (perdre du temps)
  • Tester automatiquement (rapide et plutôt efficace)
  • Prouver automatiquement (rapide et efficace)

Bon, ben "y'a plus qu'à" faire l'outil de preuve automatique :p
Ou mettre en place un système de typage "hyper fort": si la fonction prend "Integer" en entrée, elle doit faire ce qui est dit dans la doc, pour TOUT integer et si cela ne marche que pour les positifs stricts, changer de typage pour PositiveInteger.