Comment tester un système où les objets sont difficiles à imiter?

34

Je travaille avec le système suivant:

Network Data Feed -> Third Party Nio Library -> My Objects via adapter pattern

Nous avons récemment eu un problème concernant l’actualisation de la version de la bibliothèque que j’utilisais, ce qui a notamment provoqué la longmodification de l’ horodatage (renvoyé par la bibliothèque tierce ) de millisecondes après l’époque à des nanosecondes après l’époque.

Le problème:

Si j'écris des tests qui simulent les objets de la bibliothèque tierce, mon test sera faux si j'ai commis une erreur à propos des objets de la bibliothèque tierce. Par exemple, je ne savais pas que les horodatages changeaient la précision, ce qui nécessitait une modification du test unitaire, car ma maquette renvoyait les mauvaises données. Ce n'est pas un bug dans la bibliothèque , c'est parce que quelque chose dans la documentation m'a échappé.

Le problème est que je ne peux pas être sûr des données contenues dans ces structures de données car je ne peux pas en générer de vraies sans un véritable flux de données. Ces objets sont volumineux et compliqués et contiennent beaucoup de données différentes. La documentation de la bibliothèque tierce est médiocre.

La question:

Comment puis-je configurer mes tests pour tester ce comportement? Je ne suis pas sûr de pouvoir résoudre ce problème dans un test unitaire, car le test lui-même peut facilement être faux. De plus, le système intégré est volumineux et compliqué et il est facile de rater quelque chose. Par exemple, dans la situation ci-dessus, j’avais correctement ajusté la gestion de l’horodatage à plusieurs endroits, mais j’en ai manqué un. Le système semblait faire essentiellement les bons choix dans mon test d'intégration, mais lorsque je l'ai déployé en production (qui contient beaucoup plus de données), le problème est devenu évident.

Je n'ai pas de processus pour mes tests d'intégration pour le moment. Le test consiste essentiellement à: maintenir les tests unitaires correctement, ajouter d'autres tests en cas de défaillance, puis déployer sur mon serveur de test et s'assurer que les choses semblent saines, puis passer à la production. Ce problème d'horodatage a réussi les tests unitaires, car les modifications ont été créées, puis il a réussi le test d'intégration, car il ne posait aucun problème immédiat et évident. Je n'ai pas de département QA.

durron597
la source
3
Votre "enregistrement" peut-il enregistrer un véritable flux de données et le "reproduire" plus tard dans la bibliothèque tierce?
Idan Arye
2
Quelqu'un pourrait écrire un livre sur des problèmes comme celui-ci. En fait, Michael Feathers n’a écrit que ce livre: c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCode Il y décrit un certain nombre de techniques permettant de rompre les dépendances difficiles afin que le code devienne plus testable.
cbojar
2
L'adaptateur autour de la bibliothèque tierce? Oui, c'est exactement ce que je recommande. Ces tests unitaires n'amélioreront pas votre code. Ils ne le rendront pas plus fiable ou plus maintenable. Vous ne faites que dupliquer partiellement le code de quelqu'un d'autre à ce stade; dans ce cas, vous dupliquez un code mal écrit à partir de son son. C'est une perte nette. Certaines des réponses suggèrent de faire des tests d'intégration; c'est une bonne idée si vous voulez juste un "Est-ce que ça marche?" verification sanitaire. Un bon test est difficile, et cela demande autant de compétence et d'intuition qu'un bon code.
jpmc26
4
Une illustration parfaite du mal des systèmes intégrés. Pourquoi ne pas la bibliothèque retourne une Timestampclasse (contenant une représentation qu'ils veulent) et fournir des méthodes nommées ( .seconds(), .milliseconds(), .microseconds(), .nanoseconds()) et des constructeurs cours nommés. Ensuite, il n'y aurait pas eu de problèmes.
Matthieu M.
2
Le dicton "Tous les problèmes de codage peuvent être résolus par une couche d'indirection (sauf, bien sûr, le problème de trop de couches d'indirection)" vient à l'esprit ici.
Dan Pantry

Réponses:

27

On dirait que vous faites déjà preuve de diligence raisonnable. Mais ...

Au niveau le plus pratique, incluez toujours dans votre suite une bonne poignée de tests d'intégration "en boucle complète" pour votre propre code, et écrivez plus d'assertions qu'il ne vous en faut. En particulier, vous devriez avoir une poignée de tests qui effectuent un cycle complet create-read- [do_stuff] -validate.

[TestMethod]
public void MyFormatter_FormatsTimesCorrectly() {

  // this test isn't necessarily about the stream or the external interpreter.
  // but ... we depend on them working how we think they work:
  var stream = new StreamThingy();
  var interpreter = new InterpreterThingy(stream);
  stream.Write("id-123, some description, 12345");

  // this is what you're actually testing. but, it'll also hiccup
  // if your 3rd party dependencies introduce a breaking change.
  var formatter = new MyFormatter(interpreter);
  var line = formatter.getLine();
  Assert.equal(
    "some description took 123.45 seconds to complete (id-123)", line
  );
}

Et on dirait que vous faites déjà ce genre de chose. Vous ne faites que traiter avec une bibliothèque feuilletée et / ou compliquée. Et dans ce cas, il est bon d'ajouter quelques types de tests "voici comment fonctionne la bibliothèque" qui vérifient à la fois votre compréhension de la bibliothèque et servent d'exemples d'utilisation de la bibliothèque.

Supposons que vous deviez comprendre et dépendre de la manière dont un analyseur JSON interprète chaque "type" dans une chaîne JSON. Il est utile et trivial d'inclure quelque chose comme ceci dans votre suite:

[TestMethod]
public void JSONParser_InterpretsTypesAsExpected() {
  String datastream = "{nbr:11,str:"22",nll:null,udf:undefined}";
  var o = (new JSONParser()).parse(datastream);

  Assert.equal(11, o.nbr);
  Assert.equal(Int32.getType(), o.nbr.getType());
  Assert.equal("22", o.str);
  Assert.equal(null, o.nll);
  Assert.equal(Object.getType(), o.nll.getType());
  Assert.isFalse(o.KeyExists(udf));
}

Mais deuxièmement, rappelez-vous que les tests automatisés de tous types et à n'importe quel niveau de rigueur ne pourront toujours pas vous protéger contre tous les bogues. Il est parfaitement courant d'ajouter des tests au fur et à mesure que vous découvrez des problèmes. N'ayant pas de service d'assurance qualité, cela signifie que beaucoup de ces problèmes seront découverts par les utilisateurs finaux.

Et dans une large mesure, c'est juste normal.

Et troisièmement, quand une bibliothèque change la signification d'une valeur de retour ou d'un champ sans renommer le champ ou la méthode, ni autrement "casser" le code dépendant (peut-être en changeant son type), je serais vraiment mécontent de cet éditeur. Et je dirais que, même si vous auriez probablement dû lire le journal des modifications s'il en existe un, vous devriez probablement également transmettre une partie de votre stress à l'éditeur. Je dirais qu'ils ont besoin de la critique constructive, espérons-le ...

svidgen
la source
Ugh, je souhaite que ce soit aussi simple que de nourrir une chaîne JSON dans la bibliothèque. Ce n'est pas. Je ne peux pas faire l'équivalent de (new JSONParser()).parse(datastream), car ils récupèrent les données directement à partir de NetworkInterfaceet toutes les classes qui effectuent l'analyse réelle sont des paquetages privés et proguardés.
durron597
De plus, le journal des modifications n'incluait pas le fait qu'ils avaient modifié les horodatages de ms à ns, parmi les autres maux de tête qu'ils n'avaient pas documentés. Oui, je suis très mécontent d'eux et je leur en ai parlé.
durron597
@ durron597 Oh, ce n'est presque jamais le cas. Cependant, vous pouvez souvent simuler la source de données sous-jacente, comme dans le premier exemple de code. ... Point est: faire des tests d'intégration en boucle complète lorsque cela est possible, testez votre compréhension de la bibliothèque lorsque cela est possible, et juste être conscient que vous allez encore laisser les bogues dans la nature. Et vos fournisseurs tiers doivent être responsables de la réalisation de modifications invisibles et radicales.
svidgen
@ durron597 Je ne suis pas familier avec le NetworkInterface... Est-ce quelque chose que vous pouvez alimenter en données en connectant l'interface à un port sur localhost ou quelque chose du genre?
svidgen
NetworkInterface. C’est un objet de bas niveau pour travailler directement avec une carte réseau et y ouvrir des sockets, etc.
durron597
11

Réponse courte: c'est difficile. Vous avez probablement l'impression qu'il n'y a pas de bonne réponse, et c'est parce qu'il n'y a pas de réponse facile.

Réponse longue: Comme le dit @ptyx , vous avez besoin de tests du système, de tests d'intégration et de tests unitaires:

  • Les tests unitaires sont rapides et faciles à exécuter. Ils attrapent les bogues dans des sections individuelles de code et utilisent des simulacres pour les rendre possibles. Par nécessité, ils ne peuvent pas détecter les disparités entre les éléments de code (comme les millisecondes par rapport aux nanosecondes).
  • Les tests d'intégration et les tests système sont plus lents et plus difficiles à exécuter mais détectent plus d'erreurs.

Quelques suggestions spécifiques:

  • Il est avantageux de simplement faire un test du système pour le faire fonctionner le plus possible. Même s'il ne peut pas valider une grande partie du comportement ou ne réussit pas très bien à cerner le problème. (Micheal Feathers en parle davantage dans Travailler efficacement avec Legacy Code .)
  • Investir dans la testabilité aide. Vous pouvez utiliser ici un grand nombre de techniques: intégration continue, scripts, machines virtuelles, outils de lecture, proxy ou redirection du trafic réseau.
  • L'un des avantages (du moins pour moi) d'investir dans la testabilité peut ne pas être évident: si les tests sont fastidieux, ennuyeux ou fastidieux à écrire ou à exécuter, il est trop facile pour moi de les ignorer simplement si je suis sous pression ou fatigué. Il est important de garder vos tests en dessous du seuil "Il est si facile que rien n’excuse de ne pas le faire".
  • Un logiciel parfait n'est pas réalisable. Comme pour tout le reste, les efforts consacrés aux tests représentent un compromis, et parfois, ils ne valent pas la peine. Des contraintes (telles que votre absence de département d'assurance qualité) existent. Acceptez que des bugs se produisent, récupérez et apprenez.

J'ai vu la programmation décrite comme l'activité d'apprentissage d'un problème et d'un espace de solution. Avoir tout parfait à l'avance peut ne pas être réalisable, mais vous pouvez apprendre après coup. ("J'ai corrigé le traitement de l'horodatage à plusieurs endroits mais j'en ai oublié un. Puis-je modifier mes types de données ou mes classes pour rendre le traitement de l'horodatage plus explicite et plus difficile à manquer, ou pour le rendre plus centralisé afin que je ne dispose que d'un seul emplacement? Puis-je modifier Puis-je simplifier mon environnement de test pour le rendre plus facile à l'avenir? Puis-je imaginer un outil qui l'aurait facilité et, dans l'affirmative, puis-je le trouver sur Google? " Etc.)

Josh Kelley
la source
7

J'ai mis à jour la version de la bibliothèque… ce qui… a provoqué la modification des horodatages (que la bibliothèque tierce renvoie sous forme de long), de millisecondes après l'époque à des nanosecondes après l'époque.

Ce n'est pas un bug dans la bibliothèque

Je suis fortement en désaccord avec vous ici. C'est un bug dans la bibliothèque , plutôt insidieux en fait. Ils ont changé le type sémantique de la valeur de retour, mais pas le type de programmation de la valeur de retour. Cela peut causer toutes sortes de ravages, surtout s’il s’agissait d’une bosse de version mineure, mais même s’il s’agissait d’une bombe majeure.

Disons plutôt que la bibliothèque a retourné un type de MillisecondsSinceEpoch, un simple wrapper contenant un long. Quand ils ont changé une NanosecondsSinceEpochvaleur, votre code aurait échoué à la compilation et vous aurait évidemment indiqué les endroits où vous devez apporter des modifications. Le changement ne pouvait pas corrompre votre programme en silence.

Mieux encore, ce serait un TimeSinceEpochobjet qui pourrait adapter son interface à mesure que davantage de précision était ajoutée, telle que l'ajout d'une #toLongNanosecondsméthode à côté de la #toLongMillisecondsméthode, ne nécessitant aucune modification de votre code.

Le problème suivant est que vous ne disposez pas d’un ensemble fiable de tests d’intégration dans la bibliothèque. Vous devriez écrire ceux-ci. Il serait préférable de créer une interface autour de cette bibliothèque pour l’encapsuler loin du reste de votre application. Plusieurs autres réponses abordent ce problème (et d’autres continuent à apparaître au fur et à mesure que je tape). Les tests d'intégration doivent être exécutés moins souvent que vos tests unitaires. C'est pourquoi avoir une couche tampon aide. Séparez vos tests d'intégration dans une zone distincte (ou nommez-les différemment) afin de pouvoir les exécuter selon vos besoins, mais pas à chaque fois que vous exécutez votre test unitaire.

cbojar
la source
2
@ durron597 Je soutiens toujours que c'est un bogue. Au-delà du manque de documentation, pourquoi changer le comportement attendu? Pourquoi pas une nouvelle méthode qui fournit la nouvelle précision et laisse l'ancienne méthode toujours fournir des millisecondes? Et pourquoi ne pas fournir un moyen au compilateur de vous alerter par un changement de type de retour? Cela ne prend pas beaucoup pour que cela soit beaucoup plus clair, pas seulement dans la documentation, mais dans le code lui-même.
cbojar
1
@gbjbaanb, "qu'ils ont de mauvaises pratiques de publication" me semble un bug
Arturo Torres Sánchez
2
@gbjbaanb Une bibliothèque tierce [devrait] passer un "contrat" ​​avec ses utilisateurs. La rupture de ce contrat - documenté ou non - peut / devrait être considérée comme un bug. Comme d'autres l'ont déjà dit, si vous devez changer quelque chose, ajoutez au contrat une nouvelle fonction / méthode (voir toutes les ...Ex()méthodes de Win32API). Si ce n'était pas faisable, "rompre" le contrat en renommant la fonction (ou son type de retour) aurait été préférable à une modification du comportement.
TripeHound
1
C'est un bug dans la bibliothèque. L'utilisation de nanosecondes sur une longue période le pousse.
Josué
1
@ gbjbaanb Vous dites que ce n'est pas un bogue puisqu'il s'agit d'un comportement prévu, même s'il est inattendu. En ce sens, ce n'est pas un bogue d' implémentation , mais c'est un bogue identique. On pourrait appeler cela un défaut de conception ou un bogue d'interfaçage . Les défauts résident dans le fait qu’elle expose une obsession primitive avec des unités longues plutôt que des unités explicites, son abstraction est fuyant car elle exporte les détails de son implémentation interne le principe du moindre étonnement avec une unité subtile change.
cbojar
5

Vous avez besoin de tests d'intégration et de système.

Les tests unitaires permettent de vérifier que votre code se comporte comme prévu. Comme vous le réalisez, cela ne remet pas en question vos hypothèses et ne garantit pas la santé de vos attentes.

Sauf si votre produit a peu d'interaction avec des systèmes externes, ou interagit avec des systèmes si bien connus, stables et documentés qu'ils peuvent être simulés avec assurance (cela se produit rarement dans le monde réel) - les tests unitaires ne suffisent pas.

Plus vos tests sont élevés, plus ils vous protégeront des imprévus. Cela a un coût (commodité, rapidité, fragilité ...), donc les tests unitaires doivent rester la base de vos tests, mais vous avez besoin d'autres couches, y compris - éventuellement - un petit peu de tests humains qui contribueront grandement à capturer Des choses stupides auxquelles personne n'a pensé.

ptyx
la source
2

Le mieux serait de créer un prototype minimal et de comprendre le fonctionnement exact de la bibliothèque. En faisant cela, vous gagnerez des connaissances sur la bibliothèque avec une documentation médiocre. Un prototype peut être un programme minimaliste qui utilise cette bibliothèque et assure la fonctionnalité.

Sinon, cela n'a aucun sens d'écrire des tests unitaires, avec des exigences à moitié définies et une compréhension faible du système.

En ce qui concerne votre problème spécifique - à propos de l’utilisation d’indicateurs erronés: je le traiterais comme un changement d’exigences. Une fois le problème reconnu, modifiez les tests unitaires et le code.

BЈовић
la source
1

Si vous utilisiez une bibliothèque populaire et stable, alors vous pourriez peut-être supposer qu'elle ne vous jouera pas de mauvais tours. Mais si des choses comme ce que vous avez décrit se produisent avec cette bibliothèque, alors évidemment, ce n'en est pas une. Après cette mauvaise expérience, chaque fois que quelque chose ne va pas dans votre interaction avec cette bibliothèque, vous devrez examiner non seulement la possibilité que vous ayez commis une erreur, mais également la possibilité que la bibliothèque ait commis une erreur. Donc, disons qu'il s'agit d'une bibliothèque sur laquelle vous n'êtes "pas sûr".

Une des techniques employées avec les bibliothèques sur lesquelles nous ne sommes "pas sûrs" est de construire une couche intermédiaire entre notre système et lesdites bibliothèques, qui fait abstraction de la fonctionnalité offerte par les bibliothèques, affirme que nos attentes vis-à-vis de la bibliothèque sont justes et qu'elle simplifie grandement notre vie à l'avenir, devrions-nous décider de donner à cette bibliothèque le démarrage et de la remplacer par une autre bibliothèque qui se comporte mieux.

Mike Nakis
la source
Cela ne répond pas vraiment à la question. J'ai déjà une couche séparant la bibliothèque de mon système, mais le problème est que ma couche d'abstraction peut avoir des "bogues" lorsque la bibliothèque change sur moi sans avertissement.
durron597
1
@ durron597 La couche n'isole peut-être pas suffisamment la bibliothèque du reste de votre application. Si vous rencontrez des difficultés pour tester cette couche, vous devez peut-être simplifier le comportement et isoler plus fortement les données sous-jacentes.
cbojar
Qu'est-ce que @cbojar a dit En outre, permettez-moi de répéter une chose qui est peut-être passée inaperçue dans le texte ci-dessus: le assertmot-clé (ou fonction, ou fonction, en fonction de la langue que vous utilisez), est votre ami. Je ne parle pas d'assertions dans les tests unitaires / d'intégration, je dis que la couche d'isolation devrait être très lourde d'assertions, affirmant tout ce qui peut être affirmé à propos du comportement de la bibliothèque.
Mike Nakis
Ces assertions ne s'exécutent pas nécessairement sur les cycles de production, mais elles s'exécutent lors des tests, en affichant une zone blanche de votre couche d'isolation et donc en s'assurant (autant que possible) que les informations que votre couche reçoit de la bibliothèque est sain.
Mike Nakis