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 où j'ai mis à jour la version de la bibliothèque que j'utilisais, ce qui, entre autres, a provoqué des horodatages (que la bibliothèque tierce renvoie comme long
), à changer de quelques millisecondes après l'Époque à nanosecondes après l'époque.
Si j'écris des tests qui se moquent des objets de la bibliothèque tierce, mon test sera faux si j'ai fait une erreur sur les objets de la bibliothèque tierce. Par exemple, je ne savais pas que les horodatages changeaient de précision, ce qui entraînait la nécessité de modifier le test unitaire, car ma maquette renvoyait des données erronées. Ce n'est pas un bogue dans la bibliothèque , c'est arrivé parce que j'ai raté quelque chose dans la documentation.
Le problème est que je ne peux pas être sûr des données contenues dans ces structures de données parce que je ne peux pas générer de vraies sans un vrai flux de données. Ces objets sont gros et compliqués et ont beaucoup de données différentes en eux. La documentation de la bibliothèque tierce est médiocre.
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 grand 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 principalement les bonnes choses dans mon test d'intégration, mais quand 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 en ce moment. Les tests sont essentiellement: essayez de garder les tests unitaires bons, ajoutez plus de tests lorsque les choses se cassent, puis déployez-les sur mon serveur de test et assurez-vous que les choses semblent saines, puis déployez-les en production. Ce problème d'horodatage a réussi les tests unitaires parce que les mocks ont été mal créés, puis il a réussi le test d'intégration car il n'a pas causé de problèmes immédiats et évidents. Je n'ai pas de département QA.
Il semble que vous fassiez déjà preuve de diligence raisonnable. Mais ...
Au niveau le plus pratique, incluez toujours une bonne poignée des deux tests d'intégration "en boucle complète" dans votre suite pour votre propre code, et écrivez plus d'assertions que vous pensez en avoir besoin. En particulier, vous devriez avoir une poignée de tests qui effectuent un cycle complet de création-lecture- [do_stuff] -validation.
[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 avez juste affaire à une bibliothèque feuilletée et/ou compliquée. Et dans ce cas, il est bon de lancer quelques types de tests "voici comment fonctionne la bibliothèque" qui à la fois vérifient votre compréhension de la bibliothèque et servent d'exemples sur la façon d'utiliser la bibliothèque.
Supposons que vous devez comprendre et dépendre de la façon dont un analyseur JSON interprète chaque "type" dans une chaîne JSON. Il est utile et trivial d'inclure quelque chose comme ça 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 toute nature, et à presque tous les niveaux de rigueur, ne parviendront toujours pas à vous protéger contre tous les bogues. Il est parfaitement courant d'ajouter des tests lorsque 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 ou autrement "casser" le code dépendant (peut-être en changeant son type), je serais sacrément sacrément mécontent de cet éditeur. Et je dirais que, même si vous auriez probablement dû lire le changelog s'il y en a un, vous devriez probablement aussi transmettre une partie de votre stress à l'éditeur. Je dirais qu'ils ont besoin de la critique, espérons-constructive, ...
Réponse courte: c'est difficile. Vous vous sentez probablement comme s'il n'y avait pas de bonnes réponses, et c'est parce qu'il n'y a pas de réponses faciles.
Réponse longue: Comme @ ptyx dit , vous avez besoin de tests système et de tests d'intégration ainsi que de tests unitaires:
Quelques suggestions spécifiques:
J'ai vu la programmation décrite comme l'activité d'apprentissage d'un espace de problème et de solution. Il n'est peut-être pas possible d'obtenir tout ce qui est parfait à l'avance, mais vous pouvez apprendre après coup. ("J'ai corrigé la gestion de l'horodatage à plusieurs endroits mais j'en ai manqué un. Puis-je changer mes types de données ou mes classes pour rendre la gestion de l'horodatage plus explicite et plus difficile à manquer, ou pour la rendre plus centralisée afin de n'avoir qu'un seul endroit à modifier? Puis-je modifier mes tests pour vérifier plus d'aspects de la gestion de l'horodatage? Puis-je simplifier mon environnement de test pour rendre cela plus facile à l'avenir? Puis-je imaginer un outil qui aurait rendu cela plus facile, et si oui, puis-je trouver un tel outil sur Google? " Etc.)
J'ai mis à jour la version de la bibliothèque… qui… provoquait des horodatages (que la bibliothèque tierce renvoie comme
long
), changeant de millisecondes après l'Epoch en nanosecondes après l'Epoch.…
Ce n'est pas un bogue dans la bibliothèque
Je suis fortement en désaccord avec vous ici. Il est un bogue dans la bibliothèque , plutôt insidieux en fait. Ils ont changé le type sémantique de la valeur de retour, mais n'ont pas changé le type programmatique de la valeur de retour. Cela peut provoquer toutes sortes de ravages, surtout s'il s'agissait d'une bosse de version mineure, mais même si elle était majeure.
Disons plutôt que la bibliothèque a renvoyé un type de MillisecondsSinceEpoch
, un simple wrapper qui contient un long
. Quand ils l'ont changé en valeur NanosecondsSinceEpoch
, votre code n'aurait pas pu être compilé et vous aurait évidemment indiqué les endroits où vous devez apporter des modifications. Le changement n'a pas pu corrompre silencieusement votre programme.
Mieux encore serait un objet TimeSinceEpoch
qui pourrait adapter son interface à mesure que plus de précision était ajoutée, comme l'ajout d'un #toLongNanoseconds
méthode à côté du #toLongMilliseconds
méthode, ne nécessitant aucune modification de votre code.
Le problème suivant est que vous n'avez pas un ensemble fiable de tests d'intégration à la bibliothèque. Vous devriez les écrire. Mieux serait de créer une interface autour de cette bibliothèque pour l'encapsuler loin du reste de votre application. Plusieurs autres réponses traitent de cela (et d'autres continuent d'apparaître pendant que je tape). Les tests d'intégration doivent être exécutés moins fréquemment que vos tests unitaires. C'est pourquoi avoir une couche tampon est utile. 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.
Vous avez besoin d'intégration et de tests système.
Les tests unitaires sont parfaits pour vérifier que votre code se comporte comme prévu. Comme vous vous en rendez compte, cela ne remet pas en cause vos hypothèses ni ne garantit que vos attentes sont sensées.
À moins que votre produit n'interagisse peu avec des systèmes externes ou interagisse avec des systèmes si connus, stables et documentés qu'ils peuvent être moqués en toute confiance (cela se produit rarement dans le monde réel) - les tests unitaires ne suffisent pas.
Plus vos tests sont de niveau élevé, plus ils vous protégeront contre l'inattendu. Cela a un coût (commodité, vitesse, fragilité ...), donc les tests unitaires doivent rester la base de vos tests, mais vous avez besoin d'autres couches, y compris - éventuellement - un tout petit peu de tests humains qui contribuent grandement à la capture des choses stupides auxquelles personne n'a pensé.
Le mieux serait de créer un prototype minimal et de comprendre comment fonctionne exactement la bibliothèque. En faisant cela, vous gagnerez quelques connaissances sur la bibliothèque avec une mauvaise documentation. Un prototype peut être un programme minimaliste qui utilise cette bibliothèque et fait la fonctionnalité.
Sinon, cela n'a aucun sens d'écrire des tests unitaires, avec des exigences à moitié définies et une faible compréhension du système.
Quant à votre problème spécifique - à propos de l'utilisation de mesures erronées: je le traiterais comme un changement des exigences. Une fois que vous avez reconnu le problème, modifiez les tests unitaires et le code.
Si vous utilisiez une bibliothèque stable et populaire, vous pourriez peut-être supposer qu'elle ne vous jouera pas de mauvais tours. Mais si des choses comme ce que vous décrivez 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 se passe mal dans votre interaction avec cette bibliothèque, vous devrez examiner non seulement la possibilité que vous ayez fait une erreur, mais aussi, la possibilité que la bibliothèque ait pu faire une erreur. Donc, disons que c'est une bibliothèque dont vous n'êtes pas sûr.
L'une des techniques employées avec les bibliothèques dont nous sommes "incertains" consiste à construire une couche intermédiaire entre notre système et lesdites bibliothèques, qui résume les fonctionnalités offertes par les bibliothèques, affirme que nos attentes à l'égard de la bibliothèque sont correctes et simplifie également grandement notre vie à l'avenir, si nous décidons de donner à cette bibliothèque le démarrage et de la remplacer par une autre bibliothèque qui se comporte mieux.