Renforcement du code avec une gestion des exceptions éventuellement inutile


12

Est-ce une bonne pratique d'implémenter une gestion des exceptions inutile, juste au cas où une autre partie du code ne serait pas codée correctement?

Exemple de base

Un simple, donc je ne perds pas tout le monde :).

Disons que j'écris une application qui affichera les informations d'une personne (nom, adresse, etc.), les données étant extraites d'une base de données. Disons que je suis celui qui code la partie de l'interface utilisateur et que quelqu'un d'autre écrit le code de requête DB.

Imaginez maintenant que les spécifications de votre application indiquent que si les informations de la personne sont incomplètes (disons, le nom est manquant dans la base de données), la personne codant la requête doit gérer cela en retournant "NA" pour le champ manquant.

Que faire si la requête est mal codée et ne gère pas ce cas? Et si le gars qui a écrit la requête vous traite d'un résultat incomplet, et lorsque vous essayez d'afficher les informations, tout se bloque, car votre code n'est pas prêt à afficher des éléments vides?

Cet exemple est très basique. Je pense que la plupart d'entre vous diront "ce n'est pas votre problème, vous n'êtes pas responsable de cet accident". Mais, c'est toujours votre partie du code qui plante.

Un autre exemple

Disons que c'est maintenant moi qui rédige la requête. Les spécifications ne disent pas la même chose que ci-dessus, mais le gars qui écrit la requête "insert" doit s'assurer que tous les champs sont remplis lors de l'ajout d'une personne à la base de données pour éviter d'insérer des informations incomplètes. Dois-je protéger ma requête "select" pour m'assurer que je donne les informations complètes au gars de l'interface

Questions

Que se passe-t-il si les spécifications ne disent pas explicitement "c'est ce type qui est chargé de gérer cette situation"? Que faire si une troisième personne implémente une autre requête (similaire à la première, mais sur une autre base de données) et utilise votre code d'interface utilisateur pour l'afficher, mais ne gère pas ce cas dans son code?

Dois-je faire ce qui est nécessaire pour éviter un éventuel crash, même si ce n'est pas moi qui suis censé gérer le mauvais dossier?

Je ne cherche pas une réponse comme "(s) il est le seul responsable du crash", car je ne résout pas un conflit ici, j'aimerais savoir, si je protège mon code contre des situations ce n'est pas ma responsabilité gérer? Ici, un simple "si vide fait quelque chose" suffirait.

En général, cette question aborde la gestion des exceptions redondantes. Je le demande parce que lorsque je travaille seul sur un projet, je peux coder 2 à 3 fois une gestion d'exception similaire dans les fonctions successives, "juste au cas où" j'ai fait quelque chose de mal et j'ai laissé un mauvais cas passer.


4
Vous parlez de "tests", mais pour autant que je comprends votre problème, vous parlez de "tests qui sont appliqués en production", c'est mieux appelé "validation" ou "gestion des exceptions".
Doc Brown

1
Oui, le mot approprié est "gestion des exceptions".
rdurand

a ensuite changé la mauvaise étiquette
Doc Brown

Je vous renvoie au DailyWTF - êtes-vous sûr de vouloir faire ce genre de test?
gbjbaanb

@gbjbaanb: Si je comprends bien votre lien, ce n'est pas du tout ce dont je parle. Je ne parle pas de "tests stupides", je parle de la duplication de la gestion des exceptions.
rdurand

Réponses:


14

Vous parlez ici de limites de confiance . Faites-vous confiance à la frontière entre votre application et la base de données? La base de données est-elle sûre que les données de l'application sont toujours pré-validées?

C'est une décision qui doit être prise dans chaque demande et il n'y a pas de bonnes et de mauvaises réponses. J'ai tendance à pécher par excès d'appeler trop de frontières une limite de confiance, les autres développeurs se feront un plaisir de faire confiance aux API tierces pour faire ce que vous attendez d'eux, tout le temps, à chaque fois.


5

Le principe de robustesse «Soyez conservateur dans ce que vous envoyez, soyez libéral dans ce que vous acceptez» est ce que vous recherchez. C'est un bon principe - EDIT: tant que son application ne cache pas de graves erreurs - mais je suis d'accord avec @pdr que cela dépend toujours de la situation si vous devez l'appliquer ou non.


Certaines personnes pensent que le "principe de robustesse" est de la merde. L'article donne un exemple.

@MattFenwick: merci de l'avoir souligné, c'est un point valable, j'ai un peu changé ma réponse.
Doc Brown

2
Ceci est un article encore meilleur soulignant les problèmes du "principe de robustesse": joelonsoftware.com/items/2008/03/17.html
hakoja

1
@hakoja: honnêtement, je connais bien cet article, il s'agit de problèmes que vous obtenez lorsque vous commencez à ne pas suivre le principe de robustesse (comme certains gars MS ont essayé avec des versions IE plus récentes). Néanmoins, cela s'éloigne un peu de la question d'origine.
Doc Brown

1
@DocBrown: c'est exactement pourquoi vous n'auriez jamais dû être libéral dans ce que vous acceptez. La robustesse ne signifie pas que vous devez accepter tout ce qui vous est lancé sans vous plaindre, mais simplement que vous devez accepter tout ce qui vous est lancé sans vous écraser.
Marjan Venema

1

Cela dépend de ce que vous testez; mais supposons que la portée de votre test est uniquement votre propre code. Dans ce cas, vous devez tester:

  • Le «cas heureux»: alimentez votre application en entrée valide et assurez-vous qu'elle produit une sortie correcte.
  • Les cas d'échec: alimentez votre application en entrées invalides et assurez-vous qu'elle les gère correctement.

Pour ce faire, vous ne pouvez pas utiliser le composant de votre collègue: utilisez plutôt le mocking , c'est-à-dire remplacez le reste de l'application par des modules "faux" que vous pouvez contrôler à partir du framework de test. La façon exacte de procéder dépend de la façon dont les modules s'interfacent; cela peut suffire d'appeler simplement les méthodes de votre module avec des arguments codés en dur, et cela peut devenir aussi complexe que d'écrire un framework entier qui connecte les interfaces publiques des autres modules avec l'environnement de test.

C'est juste le cas de test unitaire, cependant. Vous voulez également des tests d'intégration, où vous testez tous les modules de concert. Encore une fois, vous voulez tester à la fois le cas heureux et les échecs.

Dans votre cas "Exemple de base", pour tester votre code à l'unité, écrivez une classe fictive qui simule la couche de base de données. Cependant, votre classe fictive ne va pas vraiment dans la base de données: vous la préchargez simplement avec les entrées et les sorties fixes attendues. En pseudocode:

function test_ValidUser() {
    // set up mocking and fixtures
    userid = 23;
    db = new MockDB();
    db.fixedResult = { firstName: "John", lastName: "Doe" };
    db.expectedCall = { method: 'getUser', params: { userid: userid } };
    userController = new UserController(db);
    expectedResult = "John Doe";

    // run the actual test
    actualResult = userController.displayUserAsString(userid);

    // check assertions
    assertEquals(expectedResult, actualResult);
    db.assertExpectedCall();
}

Et voici comment tester les champs manquants correctement signalés :

function test_IncompleteUser() {
    // set up mocking and fixtures
    userid = 57;
    db = new MockDB();
    db.fixedResult = { firstName: "John", lastName: "NA" };
    db.expectedCall = { method: 'getUser', params: { userid: userid } };
    userController = new UserController(db);

    // let's say the user controller is specified to leave "NA" fields 
    // blank
    expectedResult = "John";

    // run the actual test
    actualResult = userController.displayUserAsString(userid);

    // check assertions
    assertEquals(expectedResult, actualResult);
    db.assertExpectedCall();
}

Maintenant, les choses deviennent intéressantes. Et si la vraie classe DB se comporte mal? Par exemple, il pourrait lever une exception pour des raisons peu claires. Nous ne savons pas si c'est le cas, mais nous voulons que notre propre code le gère avec élégance. Pas de problème, il suffit de faire une exception à notre MockDB, par exemple en ajoutant une méthode comme celle-ci:

class MockDB {
    // ... snip
    function getUser(userid) {
        if (this.fixedException) {
            throw this.fixedException;
        }
        else {
            return this.fixedResult;
        }
    }
}

Et puis notre cas de test ressemble à ceci:

function test_MisbehavingUser() {
    // set up mocking and fixtures
    userid = 57;
    db = new MockDB();
    db.fixedException = new SQLException("You have an error in your SQL syntax");
    db.expectedCall = { method: 'getUser', params: { userid: userid } };
    userController = new UserController(db);

    // run the actual test
    try {
        userController.displayUserAsString(userid);
    }
    catch (DatabaseException ex) {
        // This is good: our userController has caught the raw exception
        // from the database layer and wrapped it in a DatabaseException.
        return TEST_PASSED;
    }
    catch (Exception ex) {
        // This is not good: we have an exception, but it's the wrong kind.
        testLog.log("Found the wrong exception: " + ex);
        return TEST_FAILED;
    }
    // This is bad, too: either our mocking class didn't throw even when it
    // should have, or our userController swallowed the exception and
    // discarded it
    testLog.log("Expected an exception to be thrown, but nothing happened.");
    return TEST_FAILED;
}

Ce sont vos tests unitaires. Pour le test d'intégration, vous n'utilisez pas la classe MockDB; au lieu de cela, vous enchaînez les deux classes réelles ensemble. Vous avez encore besoin de luminaires; par exemple, vous devez initialiser la base de données de test à un état connu avant d'exécuter le test.

Maintenant, en ce qui concerne les responsabilités: votre code doit s'attendre à ce que le reste de la base de code soit implémenté conformément aux spécifications, mais il doit également être prêt à gérer les choses avec élégance lorsque les autres se gâchent. Vous n'êtes pas responsable de tester un autre code que le vôtre, mais vous êtes responsable de faire en sorte que votre code résiste aux mauvais comportements du code à l'autre extrémité, et vous êtes également responsable du test de la résilience de votre code. C'est ce que fait le troisième test ci-dessus.


avez-vous lu les commentaires sous la question? Le PO a écrit des "tests", mais il le pensait dans le sens de "contrôles de validation" et / ou de "gestion des exceptions"
Doc Brown,

1
@tdammers: désolé pour le malentendu, je voulais dire en fait la gestion des exceptions .. Merci quand même pour la réponse complète, le dernier paragraphe est ce que je cherchais.
rdurand

1

Il y a 3 principes principaux que j'essaie de coder:

  • SEC

  • BAISER

  • YAGNI

Le hic, c'est que vous risquez d'écrire du code de validation qui est dupliqué ailleurs. Si les règles de validation changent, celles-ci devront être mises à jour à plusieurs endroits.

Bien sûr, à un moment donné dans le futur, vous pourriez remplacer votre base de données (cela arrive), auquel cas vous pourriez penser que le code à plusieurs endroits serait avantageux. Mais ... vous codez pour quelque chose qui pourrait ne pas arriver.

Tout code supplémentaire (même s'il ne change jamais) est une surcharge car il devra être écrit, lu, stocké et testé.

Tout cela étant vrai, il serait négligent de votre part de ne faire aucune validation. Pour afficher un nom complet dans l'application, vous auriez besoin de données de base - même si vous ne validez pas les données elles-mêmes.


1

Dans les mots du profane.

Il n'y a rien de tel que "la base de données" ou "l'application" .

  1. Une base de données peut être utilisée par plusieurs applications.
  2. Une application peut utiliser plusieurs bases de données.
  3. Le modèle de base de données doit appliquer l'intégrité des données, ce qui inclut l'envoi d'une erreur lorsqu'un champ obligatoire n'est pas inclus dans une opération d'insertion, sauf si une valeur par défaut est définie dans la définition de la table. Cela doit être fait même si vous insérez la ligne directement dans la base de données en contournant l'application. Laissez le système de base de données le faire pour vous.
  4. Les bases de données doivent protéger l'intégrité des données et générer des erreurs .
  5. La logique métier doit intercepter ces erreurs et lever des exceptions dans la couche de présentation.
  6. La couche de présentation doit valider l'entrée, gérer les exceptions ou montrer un triste hamster à l'utilisateur.

Encore:

  • Base de données-> jeter des erreurs
  • Business Logic-> intercepter les erreurs et lever les exceptions
  • Couche de présentation-> valider, lever des exceptions ou afficher des messages tristes.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.