Rafforzamento del codice con una gestione delle eccezioni eventualmente inutile

12

È buona pratica 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 mostrerà le informazioni di una persona (nome, indirizzo, ecc.), i dati che vengono estratti da un database. Diciamo che sono io a codificare la parte dell'interfaccia utente e qualcun altro sta scrivendo il codice di interrogazione del database.

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

Che cosa succede se la query è mal codificata e non gestisce questo caso? Cosa succede se il tizio 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 cose vuote?

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

Un altro esempio

Diciamo che sono io a scrivere la query. Le specifiche non dicono lo stesso di sopra, ma che il ragazzo che scrive la query "Inserisci" 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 assicurarmi di fornire informazioni complete sull'interfaccia utente?

Le domande

Che cosa succede se le specifiche non dicono 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 usa il codice dell'interfaccia utente per visualizzarla, ma non gestisce questo caso nel suo codice?

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

Non cerco una risposta del tipo "(s) lui è il responsabile dello schianto", poiché non sto risolvendo un conflitto qui, mi piacerebbe sapere, dovrei proteggere il mio codice dalle situazioni è non è mia responsabilità gestire? Qui, un semplice "se vuoto fai qualcosa" sarebbe sufficiente.

In generale, questa domanda affronta la gestione delle eccezioni ridondanti. Lo sto chiedendo perché quando lavoro da solo su un progetto, posso codificare 2-3 volte un'analoga gestione delle eccezioni nelle funzioni successive, "nel caso in cui" ho fatto qualcosa di sbagliato e ho fatto passare un caso negativo.

    
posta rdurand 24.05.2013 - 11:16
fonte

5 risposte

14

Quello di cui stai parlando è limiti di affidabilità . Ti fidi del confine tra l'applicazione e il database? Il database si fida che i dati dell'applicazione siano sempre pre-validati?

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

    
risposta data 24.05.2013 - 11:53
fonte
5

Il principio di robustezza "Sii prudente in ciò che invii, sii liberale in ciò che accetti" è ciò che stai cercando . È un buon principio - EDIT: finché la sua applicazione non nasconde errori gravi - ma sono d'accordo con @pdr che dipende sempre dalla situazione se dovessi applicarla o meno.

    
risposta data 24.05.2013 - 11:58
fonte
1

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

  • Il "caso felice": alimenta l'input valido della tua applicazione e assicurati che produca l'output corretto.
  • I casi di errore: invia gli input non validi della tua applicazione e assicurati che li gestisca correttamente.

Per fare questo, non puoi usare il componente del tuo collega: invece, usa mocking , cioè sostituisci il resto dell'applicazione con i moduli "finti" che puoi controllare dal framework di test . Come esattamente ciò si fa dipende dal modo in cui i moduli si interfacciano; può essere sufficiente chiamare solo i metodi del tuo modulo con argomenti hard-coded, e può diventare complesso come scrivere un intero framework che connette le interfacce pubbliche degli altri moduli con l'ambiente di testing.

Questo è solo il caso di test unitario, però. Vuoi anche test di integrazione, dove testerai tutti i moduli in concerto. Di nuovo, vuoi testare sia il caso felice che i fallimenti.

Nel tuo caso "Esempio di base", per testare il tuo codice unitario, scrivi una classe di simulazione che simula il livello del database. La tua classe di simulazione non è realmente nel database: basta precaricarla con gli input previsti e gli output fissi. In 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();
}

Ecco come testare 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. Cosa succede se la classe DB reale si comporta male? Ad esempio, potrebbe generare un'eccezione per motivi non chiari. Non sappiamo se lo fa, ma vogliamo che il nostro codice lo gestisca con garbo. Nessun problema, dobbiamo solo fare in modo che il nostro MockDB generi un'eccezione, ad es. 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 caso di test assomiglia a questo:

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 tuoi test unitari. Per il test di integrazione, non si utilizza la classe MockDB; invece, si concatenano entrambe le classi reali. Hai ancora bisogno di infissi; ad esempio, è necessario inizializzare il database di test in uno stato conosciuto prima di eseguire il test.

Ora, per quanto riguarda le responsabilità: il tuo codice dovrebbe aspettarsi che il resto della base di codice venga implementato secondo le specifiche, ma dovrebbe anche essere preparato a gestire le cose con garbo quando il resto si rovina. Non sei responsabile per testare un altro codice diverso dal tuo, ma sei responsabile di rendere il tuo codice resiliente a un codice che non rispetta l'altro, e sei anche responsabile di testare la resilienza del tuo codice. Questo è ciò che fa il terzo test sopra.

    
risposta data 24.05.2013 - 12:27
fonte
1

Ci sono 3 principi principali che provo a codificare:

  • A SECCO

  • KISS

  • YAGNI

Il problema è che si rischia di scrivere un codice di convalida duplicato altrove. Se le regole di convalida cambiano, queste dovrebbero essere aggiornate in più punti.

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

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

Se tutto quanto sopra è vero, sarebbe negligente da parte tua non fare alcuna convalida. Per visualizzare un nome completo nell'applicazione, avrai bisogno di alcuni dati di base, anche se non convalidi i dati stessi.

    
risposta data 24.05.2013 - 15:25
fonte
1

In parole povere.

Non esiste una cosa come "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 applicare l'integrità dei dati, che include l'emissione di un errore quando un campo obbligatorio non è incluso in un'operazione di inserimento, a meno che un valore predefinito non sia definito nella definizione della tabella. Questo deve essere fatto anche se si inserisce la riga direttamente nel database ignorando l'app. Lascia che sia il sistema di database a farlo per te.
  4. I database dovrebbero proteggere l'integrità dei dati e generare errori .
  5. La logica aziendale deve intercettare quegli errori e generare eccezioni al livello di presentazione.
  6. Il livello di presentazione deve convalidare l'input, gestire le eccezioni o mostrare un criceto triste all'utente.

Anche in questo caso:

  • Database- > errori di lancio
  • Business Logic- > rileva errori e passi eccezioni
  • Presentation Layer- > convalida, genera eccezioni o mostra triste messaggi.
risposta data 24.05.2013 - 21:30
fonte

Leggi altre domande sui tag