web-dev-qa-db-fra.com

Les tests unitaires auraient-ils aidé Citigroup à éviter cette erreur coûteuse?

J'ai lu à propos de ce snafu: Le bogue de programmation coûte 7 millions de dollars à Citigroup après des transactions légitimes confondues avec des données de test pendant 15 ans .

Lorsque le système a été introduit au milieu des années 1990, le code du programme a filtré toutes les transactions auxquelles des codes de succursale à trois chiffres ont été attribués de 089 à 100 et utilisé ces préfixes à des fins de test.

Mais en 1998, l'entreprise a commencé à utiliser des codes de succursale alphanumériques à mesure qu'elle développait son activité. Parmi eux se trouvaient les codes 10B, 10C et ainsi de suite, que le système traitait comme étant dans la plage exclue, et ainsi leurs transactions ont été supprimées de tous les rapports envoyés à la SEC.

(Je pense que cela illustre que l'utilisation d'un indicateur de données non explicite est ... sous-optimale. Il aurait été préférable de remplir et d'utiliser une propriété sémantiquement explicite Branch.IsLive.)

Cela mis à part, ma première réaction a été "Les tests unitaires auraient aidé ici" ... mais le feraient-ils?

J'ai récemment lu Pourquoi la plupart des tests unitaires sont des déchets avec intérêt, et donc ma question est: à quoi ressembleraient les tests unitaires qui auraient échoué lors de l'introduction des codes de branche alphanumériques?

87
Matt Evans

Demandez-vous vraiment, "les tests unitaires auraient-ils aidé ici?", Ou demandez-vous, "n'importe quel type de test aurait-il pu aider ici?".

La forme de test la plus évidente qui aurait aidé, est une affirmation de condition préalable dans le code lui-même, qu'un identifiant de branche se compose uniquement de chiffres (en supposant que c'est l'hypothèse sur laquelle le codeur s'est appuyé pour écrire le code).

Cela pourrait alors avoir échoué dans une sorte de test d'intégration, et dès que les nouveaux identifiants de branche alphanumériques sont introduits, l'assertion explose. Mais ce n'est pas un test unitaire.

Alternativement, il pourrait y avoir un test d'intégration de la procédure qui génère le rapport SEC. Ce test garantit que chaque identifiant de branche réel rapporte ses transactions (et nécessite donc une entrée dans le monde réel, une liste de tous les identifiants de branche utilisés). Ce n'est donc pas un test unitaire non plus.

Je ne vois aucune définition ou documentation des interfaces impliquées, mais il se peut que les tests unitaires ne puissent pas avoir détecté l'erreur car l'unité n'était pas défectueuse. Si l'unité est autorisée à supposer que les identifiants de branche ne sont constitués que de chiffres et que les développeurs n'ont jamais pris de décision sur ce que le code devrait faire dans le cas contraire, alors ils ne devraient pas écrire un test unitaire dans appliquer un comportement particulier dans le cas des identifiants non numériques car le test rejetterait une implémentation valide hypothétique de l'unité qui a correctement géré les identifiants de branche alphanumériques, et vous ne voulez généralement pas écrire un test unitaire qui empêche les futures implémentations et extensions valides. Ou peut-être un document écrit il y a 40 ans a défini implicitement (via une plage lexicographique en EBCDIC brut, au lieu d'une règle de classement plus conviviale) que 10B est un identifiant de test car il se situe en fait entre 089 et 100. Mais alors Il y a 15 ans, quelqu'un a décidé de l'utiliser comme un véritable identifiant, donc la "faute" ne réside pas dans l'unité qui implémente correctement la définition d'origine: elle réside dans le processus qui n'a pas remarqué que 10B est défini comme un identifiant de test et ne doit donc pas être affecté à une succursale. La même chose se produirait dans ASCII si vous définissiez 089 - 100 comme plage de test et introduisiez ensuite un identifiant 10 $ ou 1.0. Il se trouve simplement que dans EBCDIC les chiffres viennent après les lettres.

Un test unitaire (ou sans doute un test fonctionnel) qui en théorie aurait pu sauver la journée, est un test de l'unité qui génère ou valide de nouveaux identifiants de branche. Ce test affirmerait que les identifiants ne doivent contenir que des chiffres et serait écrit afin de permettre aux utilisateurs des identifiants de branche de supposer les mêmes. Ou peut-être y a-t-il une unité quelque part qui importe de vrais identifiants de branche mais ne voit jamais ceux de test, et qui pourrait être testée à l'unité pour s'assurer qu'elle rejette tous les identifiants de test (si les identifiants ne sont que trois caractères, nous pouvons tous les énumérer et comparer le comportement de le validateur à celui du filtre de test pour s'assurer qu'ils correspondent, ce qui traite de l'objection habituelle aux tests ponctuels). Ensuite, lorsque quelqu'un a changé les règles, le test unitaire aurait échoué car il contredit le comportement nouvellement requis.

Étant donné que le test était là pour une bonne raison, le point où vous devez le supprimer en raison des exigences commerciales modifiées devient une opportunité pour quelqu'un de se voir confier le travail ", trouvez chaque place dans le code qui dépend du comportement que nous voulons changement". Bien sûr, cela est difficile et donc peu fiable, ce qui ne garantirait en aucun cas la sauvegarde du jour. Mais si vous capturez vos hypothèses dans les tests des unités dont vous supposez les propriétés, alors vous vous êtes donné une chance et donc l'effort n'est pas entièrement gaspillé.

Je conviens bien sûr que si l'unité n'avait pas été définie en premier lieu avec une entrée "drôle", alors il n'y aurait rien à tester. Les divisions délicates des espaces de noms peuvent être difficiles à tester correctement, car la difficulté ne réside pas dans la mise en œuvre de votre définition amusante, mais dans le fait de s'assurer que tout le monde comprend et respecte votre définition amusante. Ce n'est pas une propriété locale d'une unité de code. De plus, changer un type de données de "une chaîne de chiffres" en "une chaîne alphanumérique" revient à faire en sorte qu'un programme basé sur ASCII gère Unicode: ce ne sera pas simple si votre code est fortement couplé à la définition d'origine, et quand le type de données est fondamental pour ce que fait le programme, puis il est souvent fortement couplé.

c'est un peu dérangeant de penser que c'est un effort largement gaspillé

Si vos tests unitaires échouent parfois (pendant que vous refactorisez, par exemple), et ce faisant, vous donnent des informations utiles (votre changement est incorrect, par exemple), alors l'effort n'a pas été gaspillé. Ce qu'ils ne font pas, c'est de tester si votre système fonctionne. Donc, si vous écrivez des tests unitaires au lieu de avoir des tests fonctionnels et d'intégration, alors vous utilisez peut-être votre temps de manière sous-optimale.

20
Steve Jessop

Les tests unitaires auraient pu détecter que les codes de branche 10B et 10C étaient incorrectement classés comme "branches de test", mais je trouve peu probable que les tests pour cette classification de branche aient été suffisamment étendus pour détecter cette erreur.

D'un autre côté, des vérifications ponctuelles des rapports générés auraient pu révéler que les branchements 10B et 10C étaient systématiquement absents des rapports beaucoup plus tôt que les 15 ans pendant lesquels le bug était désormais autorisé à rester présent.

Enfin, c'est une bonne illustration pourquoi c'est une mauvaise idée de mélanger les données de test avec les données de production réelles dans une base de données. S'ils avaient utilisé une base de données distincte contenant les données de test, il n'aurait pas été nécessaire de filtrer cela des rapports officiels et il aurait été impossible de trop filtrer.

120

Le logiciel devait gérer certaines règles commerciales. S'il y avait des tests unitaires, les tests unitaires auraient vérifié que le logiciel gérait correctement les règles métier.

Les règles commerciales ont changé.

Apparemment, personne ne s'est rendu compte que les règles métier avaient changé et personne n'a changé le logiciel pour appliquer les nouvelles règles métier. S'il y avait eu des tests unitaires, ces tests unitaires auraient dû être modifiés, mais personne ne l'aurait fait parce que personne ne s'était rendu compte que les règles commerciales avaient changé.

Donc non, les tests unitaires n'auraient pas compris cela.

L'exception serait que les tests unitaires et le logiciel aient été créés par des équipes indépendantes et que l'équipe effectuant les tests unitaires modifiait les tests pour appliquer les nouvelles règles métier. Ensuite, les tests unitaires auraient échoué, ce qui, espérons-le, aurait entraîné un changement de logiciel.

Bien sûr, dans le même cas, si seul le logiciel était modifié et non les tests unitaires, les tests unitaires échoueraient également. Chaque fois qu'un test unitaire échoue, cela ne signifie pas que le logiciel est incorrect, cela signifie que le logiciel ou le test unitaire (parfois les deux) sont erronés.

76
gnasher729

Non. C'est l'un des gros problèmes des tests unitaires: ils vous plongent dans un faux sentiment de sécurité.

Si tous vos tests réussissent, cela ne signifie pas que votre système fonctionne correctement; cela signifie tous vos tests réussissent. Cela signifie que les parties de votre conception pour lesquelles vous avez consciemment réfléchi et écrit des tests fonctionnent comme vous le pensiez consciemment, ce qui n'est vraiment pas si grave de toute façon: c'est ce dont vous faisiez vraiment attention. , il est donc très probable que vous ayez tout de même bien compris! Mais cela ne fait rien pour attraper des cas auxquels vous n'avez jamais pensé, comme celui-ci, parce que vous n'avez jamais pensé à écrire un test pour eux. (Et si vous l'aviez fait, vous auriez été conscient que cela signifiait des modifications de code étaient nécessaires et vous les auriez modifiées.)

29
Mason Wheeler

Non pas forcément.

L'exigence initiale était d'utiliser des codes de branche numériques, donc un test unitaire aurait été produit pour un composant qui acceptait divers codes et rejetait tout comme 10B. Le système aurait été adopté comme fonctionnant (ce qu'il était).

Ensuite, l'exigence aurait changé et les codes mis à jour, mais cela aurait signifié que le code de test unitaire qui fournissait les mauvaises données (qui sont maintenant de bonnes données) aurait dû être modifié.

Maintenant, nous supposons que les personnes qui gèrent le système sauraient que c'est le cas et changeront le test unitaire pour gérer les nouveaux codes ... mais s'ils savaient que cela se produisait, ils auraient également su changer le code qui les traitait. les codes de toute façon .. et ils ne l'ont pas fait. Un test unitaire qui a initialement rejeté le code 10B aurait dit avec bonheur "tout va bien ici" lors de l'exécution, si vous ne saviez pas mettre à jour ce test.

Les tests unitaires sont bons pour le développement d'origine, mais pas pour les tests de système, surtout pas 15 ans après que les exigences ont été oubliées depuis longtemps.

Ce dont ils ont besoin dans ce genre de situation, c'est d'un test d'intégration de bout en bout. Celui où vous pouvez transmettre les données que vous prévoyez de travailler et voir si c'est le cas. Quelqu'un aurait remarqué que leurs nouvelles données d'entrée n'ont pas produit de rapport et aurait ensuite enquêté plus avant.

10
gbjbaanb

Les tests de type (le processus de test des invariants en utilisant des données valides générées de manière aléatoire, comme illustré par la bibliothèque de tests Haskell QuickCheck et divers ports/alternatives inspirés par celui-ci dans d'autres langues) peuvent bien avoir détecté ce problème, unité les tests n'auraient presque certainement pas été effectués.

En effet, lorsque les règles de validité des codes de succursale ont été mises à jour, il est peu probable que quiconque ait pensé à tester ces plages spécifiques pour s'assurer qu'elles fonctionnent correctement.

Cependant, si des tests de type avaient été utilisés, quelqu'un devrait au moment où le système d'origine a été implémenté avoir écrit une paire de propriétés, une pour vérifier que le des codes spécifiques pour les branches de test ont été traités comme des données de test et un pour vérifier qu'aucun autre code n'était ... lorsque la définition du type de données pour le code de branche a été mise à jour (ce qui aurait été nécessaire pour permettre de tester que l'une des modifications pour le code de branche du numérique au numérique a fonctionné), ce test aurait commencé à tester les valeurs dans la nouvelle plage et aurait probablement identifié le défaut.

Bien sûr, QuickCheck a été développé pour la première fois en 1999, il était donc déjà trop tard pour détecter ce problème.

8
Jules

Je doute vraiment que les tests unitaires feraient une différence dans ce problème. Cela ressemble à l'une de ces situations de vision en tunnel car la fonctionnalité a été modifiée pour prendre en charge les nouveaux codes de branche, mais cela n'a pas été effectué dans toutes les zones du système.

Nous utilisons des tests unitaires pour concevoir une classe. Une nouvelle exécution d'un test unitaire n'est requise que si la conception a changé. Si une unité particulière ne change pas, les tests unitaires inchangés renverront les mêmes résultats qu'auparavant. Les tests unitaires ne vont pas vous montrer les impacts des modifications apportées à d'autres unités (si c'est le cas, vous n'écrivez pas de tests unitaires).

Vous ne pouvez raisonnablement détecter ce problème que via:

  • Tests d'intégration - mais vous devrez ajouter spécifiquement les nouveaux formats de code pour alimenter plusieurs unités du système (c'est-à-dire qu'ils ne vous montreraient le problème que si les tests d'origine incluaient les branches désormais valides)
  • Tests de bout en bout - l'entreprise doit exécuter un test de bout en bout qui intègre les anciens et les nouveaux formats de code de succursale

Ne pas avoir suffisamment de tests de bout en bout est plus inquiétant. Vous ne pouvez pas compter sur les tests unitaires comme votre test UNIQUEMENT ou PRINCIPAL pour les modifications du système. On dirait qu'il ne fallait que quelqu'un pour exécuter un rapport sur les formats de code de branche nouvellement pris en charge.

5
Class Skeleton

La conclusion à en tirer est de Fail Fast .

Nous n'avons pas le code, ni de nombreux exemples de préfixes qui sont ou ne sont pas des préfixes de branche de test selon le code. Tout ce que nous avons c'est ceci:

  • 089 - 100 => branche de test
  • 10B, 10C => branche de test
  • <088 => vraisemblablement de vraies branches
  • > 100 => vraisemblablement de vraies branches

Le fait que le code autorise les nombres et les chaînes est plus qu'un peu étrange. Bien sûr, 10B et 10C peuvent être considérés comme des nombres hexadécimaux, mais si les préfixes sont tous traités comme des nombres hexadécimaux, 10B et 10C se situent en dehors de la plage de test et seraient traités comme de vraies branches.

Cela signifie probablement que le préfixe est stocké sous forme de chaîne mais traité comme un nombre dans certains cas. Voici le code le plus simple auquel je puisse penser qui reproduit ce comportement (en utilisant C # à des fins d'illustration):

bool IsTest(string strPrefix) {
    int iPrefix;
    if(int.TryParse(strPrefix, out iPrefix))
        return iPrefix >= 89 && iPrefix <= 100;
    return true; //here is the problem
}

En anglais, si la chaîne est un nombre compris entre 89 et 100, c'est un test. Si ce n'est pas un nombre, c'est un test. Sinon, ce n'est pas un test.

Si le code suit ce modèle, aucun test unitaire ne l'aurait détecté au moment du déploiement du code. Voici quelques exemples de tests unitaires:

assert.isFalse(IsTest("088"))
assert.isTrue(IsTest("089"))
assert.isTrue(IsTest("095"))
assert.isTrue(IsTest("100"))
assert.isFalse(IsTest("101"))
assert.isTrue(IsTest("10B")) // <--- business rule change

Le test unitaire montre que "10B" doit être traité comme une branche de test. L'utilisateur @ gnasher729 ci-dessus dit que les règles métier ont changé et c'est ce que montre la dernière assertion ci-dessus. À un moment donné, cette assertion aurait dû basculer vers un isFalse, mais cela ne s'est pas produit. Les tests unitaires sont exécutés au moment du développement et de la construction, mais à aucun moment par la suite.


Quelle est la leçon ici? Le code a besoin d'un moyen de signaler qu'il a reçu une entrée inattendue. Voici une autre façon d'écrire ce code qui souligne qu'il s'attend à ce que le préfixe soit un nombre:

// Alternative A
bool TryGetIsTest(string strPrefix, out bool isTest) {
    int iPrefix;
    if(int.TryParse(strPrefix, out iPrefix)) {
        isTest = iPrefix >= 89 && iPrefix <= 100;
        return true;
    }
    isTest = true; //this is just some value that won't be read
    return false;
}

Pour ceux qui ne connaissent pas C #, la valeur de retour indique si le code a pu ou non analyser un préfixe de la chaîne donnée. Si la valeur de retour est vraie, le code appelant peut utiliser la variable isTest out pour vérifier si le préfixe de branche est un préfixe de test. Si la valeur de retour est fausse, le code appelant doit signaler que le préfixe donné n'est pas attendu et la variable isTest out n'a pas de sens et doit être ignorée.

Si vous êtes d'accord avec les exceptions, vous pouvez le faire à la place:

// Alternative B
bool IsTest(string strPrefix) {
    int iPrefix = int.Parse(strPrefix);
    return iPrefix >= 89 && iPrefix <= 100;
}

Cette alternative est plus simple. Dans ce cas, le code appelant doit intercepter l'exception. Dans les deux cas, le code devrait avoir un moyen de signaler à l'appelant qu'il ne s'attendait pas à un strPrefix qui ne pourrait pas être converti en entier. De cette façon, le code échoue rapidement et la banque peut rapidement trouver le problème sans l'embarras de la SEC.

2
user2023861

Une assertion intégrée à l'exécution aurait pu aider; par exemple:

  1. Créez une fonction comme bool isTestOnly(string branchCode) { ... }
  2. Utilisez cette fonction pour décider quels rapports filtrer
  3. Réutilisez cette fonction dans une assertion, dans le code de création de branche, pour vérifier ou affirmer qu'une branche n'est pas (ne peut pas) être créée à l'aide de ce type de code de branche‼
  4. Ayez cette assertion activée dans le temps réel d'exécution (et non "optimisée sauf dans la version développeur du code uniquement pour le débogage")‼

Voir également:

2
ChrisW

Tant de réponses et pas même une citation de Dijkstra:

Les tests montrent la présence, pas l'absence de bugs.

Par conséquent, cela dépend. Si le code a été testé correctement, ce bogue n'existerait probablement pas.

1
BЈовић