Rafforzare il codice con una gestione delle eccezioni forse inutile


12

È buona norma implementare una gestione delle eccezioni inutile, nel caso in cui un'altra parte del codice non sia codificata correttamente?

Esempio di base

Semplice, quindi non perdo tutti :).

Diciamo che sto scrivendo un'app che visualizzerà le informazioni di una persona (nome, indirizzo, ecc.), I dati estratti da un database. Diciamo che sono io a codificare la parte dell'interfaccia utente e qualcun altro sta scrivendo il codice query DB.

Ora immagina che le specifiche della tua app affermino che se le informazioni della persona sono incomplete (diciamo, il nome è mancante nel database), la persona che codifica la query dovrebbe gestirlo restituendo "NA" per il campo mancante.

Che cosa succede se la query è scarsamente codificata e non gestisce questo caso? Che cosa succede se il tipo che ha scritto la query ti gestisce un risultato incompleto e quando provi a visualizzare le informazioni, tutto si blocca, perché il tuo codice non è pronto a mostrare elementi vuoti?

Questo esempio è molto semplice. Credo che molti di voi diranno "non è un tuo problema, non sei responsabile di questo incidente". Ma è ancora la tua parte del codice che sta andando in crash.

Un altro esempio

Diciamo ora che sono io a scrivere la domanda. Le specifiche non dicono lo stesso di cui sopra, ma il tipo che scrive la query "insert" dovrebbe assicurarsi che tutti i campi siano completi quando si aggiunge una persona al database per evitare di inserire informazioni incomplete. Devo proteggere la mia query "select" per essere sicuro di fornire al ragazzo dell'interfaccia utente informazioni complete?

Le domande

E se le specifiche non dicessero esplicitamente "questo ragazzo è il responsabile della gestione di questa situazione"? Cosa succede se una terza persona implementa un'altra query (simile alla prima, ma su un altro DB) e utilizza il tuo codice UI per visualizzarlo, ma non gestisce questo caso nel suo codice?

Devo fare ciò che è necessario per prevenire un possibile arresto, anche se non sono io quello che dovrebbe gestire il brutto caso?

Non sto cercando una risposta come "(s) è lui il responsabile dell'incidente", poiché non sto risolvendo un conflitto qui, mi piacerebbe sapere, se dovessi proteggere il mio codice da situazioni non è mia responsabilità gestire? Qui sarebbe sufficiente un semplice "se vuoto fai qualcosa".

In generale, questa domanda affronta la gestione ridondante delle eccezioni. Lo sto chiedendo perché quando lavoro da solo su un progetto, posso codificare 2-3 volte una simile eccezione gestendo funzioni successive, "per ogni evenienza" ho fatto qualcosa di sbagliato e ho lasciato passare un brutto caso.


4
Stai parlando di "test", ma per quanto ho capito il tuo problema intendi "test applicati in produzione", questo è meglio chiamato "validazione" o "gestione delle eccezioni".
Doc Brown,

1
Sì, la parola appropriata è "gestione delle eccezioni".
rdurand,

ha cambiato il tag sbagliato allora
Doc Brown,

Ti rimando a The DailyWTF : sei sicuro di voler fare questo tipo di test?
gbjbaanb,

@gbjbaanb: se capisco correttamente il tuo link, non è affatto quello di cui sto parlando. Non sto parlando di "stupidi test", sto parlando di duplicare la gestione delle eccezioni.
rdurand,

Risposte:


14

Quello di cui stai parlando qui sono i confini della fiducia . Ti fidi del confine tra la tua applicazione e il database? Il database ritiene che i dati dell'applicazione siano sempre pre-validati?

Questa è una decisione che deve essere presa in ogni domanda e non ci sono risposte giuste o sbagliate. Tendo ad errare sul lato di chiamare troppi confini un limite di fiducia, altri sviluppatori si fideranno felicemente di API di terze parti per fare ciò che ti aspetti che facciano, sempre, ogni volta.


5

Il principio di solidità "Sii conservatore in ciò che invii, sii liberale in ciò che accetti" è ciò che cerchi. È un buon principio - EDIT: fintanto che la sua applicazione non nasconde errori gravi - ma sono d'accordo con @pdr che dipende sempre dalla situazione se dovresti applicarlo o meno.


Alcune persone pensano che il "principio di robustezza" sia una schifezza. L'articolo fornisce un esempio.

@MattFenwick: grazie per averlo sottolineato, è un punto valido, ho cambiato un po 'la mia risposta.
Doc Brown,

2
Questo è un articolo ancora migliore che evidenzia i problemi con il "principio di robustezza": joelonsoftware.com/items/2008/03/17.html
hakoja

1
@hakoja: onestamente, conosco bene questo articolo, si tratta di problemi che si verificano quando si inizia a non seguire il principio di robustezza (come alcuni ragazzi MS hanno provato con le nuove versioni di IE). Tuttavia, questo si allontana un po 'dalla domanda originale.
Doc Brown,

1
@DocBrown: ecco perché non avresti mai dovuto essere liberale in ciò che accetti. Robustezza non significa che devi accettare tutto ciò che ti viene lanciato senza lamentarti, solo che devi accettare tutto ciò che ti viene lanciato senza schiantarti.
Marjan Venema,

1

Dipende da cosa stai testando; ma supponiamo che l'ambito del test sia solo il tuo codice. In tal caso, dovresti testare:

  • Il "caso felice": alimentare l'input valido dell'applicazione e assicurarsi che produca un output corretto.
  • I casi di errore: alimentare gli input non validi dell'applicazione e assicurarsi che li gestisca correttamente.

Per fare ciò, non è possibile utilizzare il componente del collega: utilizzare invece il derisione , ovvero sostituire il resto dell'applicazione con moduli "falsi" che è possibile controllare dal framework di test. Come esattamente lo fai dipende dall'interfaccia dei moduli; può essere sufficiente chiamare i metodi del tuo modulo con argomenti codificati, e può diventare tanto complesso quanto scrivere un intero framework che collega le interfacce pubbliche degli altri moduli con l'ambiente di test.

Questo è solo il caso di test unitario, però. Volete anche test di integrazione, in cui testate tutti i moduli in concerto. Ancora una volta, vuoi testare sia il caso felice che i fallimenti.

Nel caso "Esempio di base", per testare l'unità del codice, scrivi una classe simulata che simula il livello del database. La tua classe simulata non va davvero al database: devi solo pre-caricarla con input previsti e output fissi. In pseudocodice:

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();
}

Ed ecco come testeresti i campi mancanti segnalati correttamente :

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();
}

Ora le cose diventano interessanti. E se la vera classe DB si comporta male? Ad esempio, potrebbe generare un'eccezione per motivi poco chiari. Non sappiamo se lo fa, ma vogliamo che il nostro codice lo gestisca con grazia. Nessun problema, dobbiamo solo fare in modo che il nostro MockDB generi un'eccezione, ad esempio aggiungendo un metodo come questo:

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

E quindi il nostro test case si presenta così:

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;
}

Questi sono i test unitari. Per il test di integrazione, non utilizzare la classe MockDB; invece, concatenate entrambe le classi reali insieme. Hai ancora bisogno di infissi; ad esempio, è necessario inizializzare il database di test su uno stato noto prima di eseguire il test.

Ora, per quanto riguarda le responsabilità: il tuo codice dovrebbe aspettarsi che il resto del codebase venga implementato secondo le specifiche, ma dovrebbe anche essere pronto a gestire le cose con grazia quando il resto si rovina. Tu non sei responsabile per la sperimentazione altro codice diverso dal proprio, ma tu sei responsabile per rendere il vostro codice di comportamento anomalo resiliente per il codice su l'altra estremità, e si è anche responsabile per testare la resistenza del vostro codice. Questo è ciò che fa il terzo test sopra.


hai letto i commenti sotto la domanda? L'OP scrisse "test", ma intendeva nel senso di "controlli di validazione" e / o "gestione delle eccezioni"
Doc Brown,

1
@tdammers: scusate il malinteso, intendevo in effetti la gestione delle eccezioni .. Grazie comunque per la risposta completa, l'ultimo paragrafo è quello che stavo cercando.
rdurand,

1

Ci sono 3 principi principali che cerco di codificare per:

  • ASCIUTTO

  • BACIO

  • YAGNI

La cosa importante è che rischi di scrivere un codice di convalida duplicato altrove. Se le regole di convalida cambiano, queste dovrebbero essere aggiornate in più punti.

Naturalmente, ad un certo punto in futuro, potresti sostituire il tuo database (succede) nel qual caso potresti pensare che avere il codice in più di un posto sarebbe vantaggioso. Ma ... stai codificando qualcosa che potrebbe non accadere.

Qualsiasi codice aggiuntivo (anche se non cambia mai) è sovraccarico in quanto dovrà essere scritto, letto, archiviato e testato.

Tutto quanto sopra è vero, sarebbe per te una mancanza di validazione. Per visualizzare un nome completo nell'applicazione, sono necessari alcuni dati di base, anche se non si convalidano i dati stessi.


1

Nelle parole di laici.

Non esiste "il database" o "l'applicazione" .

  1. Un database può essere utilizzato da più di un'applicazione.
  2. Un'applicazione può utilizzare più di un database.
  3. Il modello di database dovrebbe imporre l'integrità dei dati, incluso il lancio di un errore quando un campo obbligatorio non è incluso in un'operazione di inserimento, a meno che nella definizione della tabella non sia definito un valore predefinito. Questo deve essere fatto anche se si inserisce la riga direttamente nel database ignorando l'app. Lascia che il sistema di database lo faccia per te.
  4. I database dovrebbero proteggere l'integrità dei dati e generare errori .
  5. La logica aziendale deve rilevare tali errori e generare eccezioni al livello di presentazione.
  6. Il livello di presentazione deve convalidare l'input, gestire le eccezioni o mostrare all'utente un criceto triste.

Ancora:

  • Database-> errori di lancio
  • Logica aziendale-> rileva errori e genera eccezioni
  • Livello presentazione-> convalida, genera eccezioni o mostra messaggi tristi.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.