Comment les erreurs de frappe seraient-elles détectées lors de la création de simulations dans un langage dynamique?

10

Le problème se produit lors de l'exécution de TDD. Après quelques tests réussis, les types de retour de certaines classes / modules changent. Dans un langage de programmation typé statiquement, si un objet simulé précédent a été utilisé dans les tests d'une autre classe et n'a pas été modifié pour refléter le changement de type, des erreurs de compilation se produisent.

Cependant, pour les langages dynamiques, la modification des types de retour peut ne pas être détectée et les tests de l' autre classe réussiront toujours. Bien sûr, il pourrait y avoir des tests d'intégration qui devraient échouer plus tard, mais les tests unitaires passeraient par erreur. Existe-t-il un moyen d'éviter cela?

Mise à jour avec un échantillon trivial (sur certains langages inventés) ...

Version 1:

Calc = {
    doMultiply(x, y) {return x * y}
}
//.... more code ....

// On some faraway remote code on a different file
Rect = {
    computeArea(l, w) {return Calc.doMultipy(x*y)}
}

// test for Rect
testComputeArea() { 
    Calc = new Mock()
    Calc.expect(doMultiply, 2, 30) // where 2 is the arity
    assertEqual(30, computeArea)
}

Maintenant, sur la version 2:

// I change the return types. I also update the tests for Calc
Calc = {
    doMultiply(x, y) {return {result: (x * y), success:true}}
}

... Rect lèvera alors une exception lors de l'exécution, mais le test réussira quand même.

jvliwanag
la source
1
Jusqu'à présent, les réponses semblent manquer, c'est que la question ne concerne pas les tests impliquant le changement class X, mais dont les tests class Ydépendent Xet sont donc testés par rapport à un contrat différent de celui auquel il se heurte en production.
Bart van Ingen Schenau
Je viens de poser cette question sur SO , moi-même, en ce qui concerne l'injection de dépendance. Voir Raison 1: une classe dépendante peut être modifiée au moment de l'exécution (pensez à tester) . Nous avons tous les deux le même état d'esprit mais manquons de bonnes explications.
Scott Coates
Je relis votre question, mais je suis un peu confus quant à l'interprétation. Pouvez vous donner un exemple?
Scott Coates

Réponses:

2

Dans une certaine mesure, cela ne représente qu'une partie du coût de faire des affaires avec des langages dynamiques. Vous obtenez beaucoup de flexibilité, autrement connu comme "assez de corde pour vous accrocher". Soyez prudent avec ça.

Pour moi, le problème suggère d'utiliser des techniques de refactorisation différentes de celles que vous utiliseriez dans un langage typé statiquement. Dans un langage statique, vous remplacez en partie les types de retour afin de pouvoir "vous appuyer sur le compilateur" pour trouver les emplacements susceptibles de se briser. Il est sûr de le faire et probablement plus sûr que de modifier le type de retour en place.

Dans un langage dynamique, vous ne pouvez pas faire cela, il est donc beaucoup plus sûr de modifier le type de retour en place, plutôt que de le remplacer. Vous pouvez éventuellement le modifier en y ajoutant votre nouvelle classe en tant que membre, pour les classes qui en ont besoin.

tallseth
la source
2

Si votre code change et que vos tests réussissent toujours, alors il y a quelque chose qui ne va pas avec vos tests (c'est-à-dire que vous manquez une assertion), ou le code n'a pas réellement changé.

Qu'est-ce que je veux dire par là? Eh bien, vos tests décrivent les contrats entre les parties de votre code. Si le contrat est "la valeur de retour doit être itérable", le changement de la valeur de retour d'un tableau par exemple à une liste n'est pas en fait un changement de contrat et ne déclenchera donc pas nécessairement un échec de test.

Afin d'éviter les affirmations manquantes, vous pouvez utiliser des outils tels que l' analyse de couverture de code (il ne sera pas vous dire quelles parties de votre code sont testés, mais il va vous dire quelles sont les parties certainement ne sont pas testées), la complexité cyclomatique et de la complexité NPATH (qui vous donnent une limite inférieure sur le nombre d'assertions nécessaires pour obtenir une couverture complète des codes C1 et C2) et des testeurs de mutation (qui injectent des mutations dans votre code telles que se transformer trueen false, des nombres négatifs en positifs, des objets en nulletc. et vérifier si cela fait échouer les tests).

Jörg W Mittag
la source
+1 Soit quelque chose de mal avec les tests ou le code n'a pas réellement changé. Cela m'a pris un certain temps pour m'y habituer après des années de C ++ / Java. Le contrat entre les parties dans les langages dynamiques du code ne doit pas être CE QUI est retourné, mais ce que contient la chose retournée et ce qu'elle peut faire.
MrFox
@suslik: En fait, il ne s'agit pas de langages statiques ou dynamiques, mais plutôt de l'abstraction de données à l'aide de types de données abstraits par rapport à l'abstraction de données orientée objet. La capacité d'un objet à simuler un autre objet tant qu'il se comporte de manière indiscernable (même s'il s'agit de types complètement différents et d'instances de classes complètement différentes) est fondamentale pour toute l'idée d'OO. Ou en d'autres termes: si deux objets se comportent de la même manière, ils sont du même type, indépendamment de ce que disent leurs classes. Autrement dit, les classes ne sont pas des types. Malheureusement, Java et C # se trompent.
Jörg W Mittag
En supposant que l'unité simulée est couverte à 100% et qu'il n'y a aucune assertion manquante: que diriez-vous d'un changement plus subtil comme "devrait retourner null pour foo" à "devrait retourner une chaîne vide pour foo". Si quelqu'un change cette unité et son test, certaines unités consommatrices peuvent se casser et à cause de la simulation, cela est transparent. (Et non: au moment de l'écriture ou de l'utilisation du module simulé, par "contrat", il n'est pas nécessaire de gérer les chaînes vides comme valeur de retour car il est censé renvoyer des chaînes non vides ou null uniquement;)). (Seuls les tests d'intégration appropriés des deux modules en interaction pourraient détecter cela.)
try-catch-finally