Wzmocnienie kodu z możliwie bezużyteczną obsługą wyjątków

12

Czy dobrą praktyką jest wdrażanie bezużytecznej obsługi wyjątków, na wypadek, gdyby inna część kodu nie została poprawnie zakodowana?

Podstawowy przykład

Prosty, więc nie tracę wszystkich :).

Powiedzmy, że piszę aplikację, która wyświetli informacje o osobie (imię i nazwisko, adres itp.), Dane zostaną wyodrębnione z bazy danych. Powiedzmy, że to ja koduję część interfejsu użytkownika, a ktoś inny pisze kod zapytania DB.

Teraz wyobraź sobie, że specyfikacje Twojej aplikacji mówią, że jeśli dane osoby są niekompletne (powiedzmy, że brakuje nazwy w bazie danych), osoba kodująca zapytanie powinna to obsłużyć, zwracając „NA” dla brakującego pola.

Co jeśli zapytanie jest źle zakodowane i nie obsługuje tego przypadku? Co jeśli facet, który napisał zapytanie, poradzi sobie z niekompletnym wynikiem, a gdy spróbujesz wyświetlić informacje, wszystko się zawiesi, ponieważ twój kod nie jest przygotowany do wyświetlania pustych rzeczy?

Ten przykład jest bardzo prosty. Wierzę, że większość z was powie „to nie jest twój problem, nie jesteś odpowiedzialny za tę awarię”. Ale to wciąż twoja część kodu ulega awarii.

Inny przykład

Powiedzmy teraz, że to ja piszę zapytanie. Specyfikacje nie mówią tego samego, co powyżej, ale że facet piszący zapytanie „wstaw” powinien upewnić się, że wszystkie pola są wypełnione podczas dodawania osoby do bazy danych, aby uniknąć wstawienia niepełnych informacji. Czy powinienem chronić moje zapytanie „wybierz”, aby upewnić się, że przekazuję facetowi interfejsu użytkownika pełne informacje?

Pytania

Co się stanie, jeśli specyfikacje nie mówią wprost „ten facet jest odpowiedzialny za poradzenie sobie z tą sytuacją”? Co się stanie, jeśli osoba trzecia zaimplementuje inne zapytanie (podobne do pierwszego, ale na innym DB) i użyje twojego kodu interfejsu użytkownika, aby go wyświetlić, ale nie obsługuje tego przypadku w swoim kodzie?

Czy powinienem zrobić wszystko, co konieczne, aby zapobiec możliwej awarii, nawet jeśli to nie ja mam zająć się złym przypadkiem?

Nie szukam odpowiedzi typu „on (on) jest odpowiedzialny za awarię”, ponieważ nie rozwiązuję tutaj konfliktu, chciałbym wiedzieć, czy powinienem chronić swój kod przed sytuacjami, to nie moja odpowiedzialność Poradzić sobie? Tutaj wystarczyłoby proste „jeśli coś pustego zrobić”.

Zasadniczo to pytanie dotyczy zbędnej obsługi wyjątków. Pytam o to, ponieważ kiedy pracuję sam nad projektem, mogę kodować 2-3 razy podobną obsługę wyjątków w kolejnych funkcjach, „na wszelki wypadek” zrobiłem coś złego i przepuściłem zły przypadek.

rdurand
źródło
4
Mówisz o „testach”, ale o ile rozumiem twój problem, masz na myśli „testy stosowane w produkcji”, lepiej to nazywa się „sprawdzaniem poprawności” lub „obsługą wyjątków”.
Doc Brown
1
Tak, odpowiednim słowem jest „obsługa wyjątków”.
rdurand
zmienił wtedy niewłaściwy tag
Doc Brown
Odsyłam Cię do DailyWTF - czy na pewno chcesz przeprowadzić tego rodzaju testy?
gbjbaanb
@gbjbaanb: Jeśli dobrze rozumiem twój link, to nie o to mi chodzi. Nie mówię o „głupich testach”, mówię o powielaniu obsługi wyjątków.
rdurand

Odpowiedzi:

14

Mówisz tutaj o granicach zaufania . Czy ufasz granicy między aplikacją a bazą danych? Czy baza danych ufa, że ​​dane z aplikacji są zawsze wstępnie sprawdzane?

Jest to decyzja, która musi być podjęta w każdej aplikacji i nie ma dobrych i złych odpowiedzi. Zwykle mylę się po stronie nazwania zbyt wielu granic granicą zaufania, inni programiści z radością ufają interfejsom API innych firm, aby robili to, czego oczekujesz, przez cały czas, za każdym razem.

pdr
źródło
5

Zasada solidności „Bądź konserwatywny w tym, co wysyłasz, bądź liberalny w tym, co akceptujesz” jest tym, czego szukasz. Jest to dobra zasada - EDYCJA: dopóki jej aplikacja nie ukrywa poważnych błędów - ale zgadzam się z @pdr, że zawsze zależy od sytuacji, czy powinieneś ją zastosować, czy nie.

Doktor Brown
źródło
Niektórzy uważają, że „zasada solidności” to bzdura. Artykuł podaje przykład.
@MattFenwick: dziękuję za zwrócenie na to uwagi, jest to ważny punkt, zmieniłem nieco moją odpowiedź.
Doc Brown
2
To jest jeszcze lepszy artykuł wskazujący na problemy z „zasadą niezawodności”: joelonsoftware.com/items/2008/03/17.html
hakoja
1
@hakoja: szczerze, znam ten artykuł dobrze, to o problemach można uzyskać po uruchomieniu nie przestrzegać zasady solidności (jak niektóre MS faceci próbowali z nowszymi wersjami IE). Niemniej jednak odbiega to nieco od pierwotnego pytania.
Doc Brown
1
@DocBrown: właśnie dlatego nigdy nie powinieneś być liberalny w tym, co akceptujesz. Wytrzymałość nie oznacza, że ​​musisz zaakceptować wszystko, co jest na ciebie rzucone bez narzekań, tylko że musisz zaakceptować wszystko, co na ciebie rzucone, bez awarii.
Marjan Venema
1

To zależy od tego, co testujesz; ale załóżmy, że zakresem twojego testu jest tylko twój własny kod. W takim przypadku powinieneś przetestować:

  • „Szczęśliwy przypadek”: podaj poprawne dane wejściowe do aplikacji i upewnij się, że generują prawidłowe dane wyjściowe.
  • Przypadki awarii: podaj nieprawidłowe dane aplikacji i upewnij się, że poprawnie je obsługuje.

W tym celu nie można użyć komponentu kolegi: zamiast tego należy użyć kpiny , czyli zastąpić resztę aplikacji „fałszywymi” modułami, którymi można sterować z poziomu środowiska testowego. To, jak dokładnie to zrobisz, zależy od sposobu interfejsu modułów; wystarczy po prostu wywołać metody modułu z zakodowanymi na stałe argumentami i może stać się tak skomplikowane, jak napisanie całego frameworka, który łączy publiczne interfejsy innych modułów ze środowiskiem testowym.

Ale to tylko przypadek testu jednostkowego. Chcesz także testów integracyjnych, w których wszystkie moduły będą testowane wspólnie. Ponownie, chcesz przetestować zarówno szczęśliwy przypadek, jak i niepowodzenia.

W przypadku „Podstawowego przykładu”, aby przetestować kod w jednostce, napisz próbną klasę, która symuluje warstwę bazy danych. Twoja próbna klasa tak naprawdę nie trafia do bazy danych: po prostu wstępnie załaduj ją oczekiwanymi danymi wejściowymi i stałymi danymi wyjściowymi. W pseudokodzie:

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

A oto jak przetestowałbyś brakujące pola, które są poprawnie zgłaszane :

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

Teraz sprawy stają się interesujące. Co się stanie, jeśli prawdziwa klasa DB zachowa się źle? Na przykład może zgłosić wyjątek z niejasnych powodów. Nie wiemy, czy tak, ale chcemy, aby nasz własny kod obsługiwał go z wdziękiem. Nie ma problemu, wystarczy, że nasz MockDB zgłosi wyjątek, np. Dodając taką metodę:

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

A potem nasz przypadek testowy wygląda następująco:

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

To są twoje testy jednostkowe. Do testu integracji nie używasz klasy MockDB; zamiast tego łączysz obie rzeczywiste klasy razem. Nadal potrzebujesz osprzętu; na przykład przed uruchomieniem testu należy zainicjować testową bazę danych do znanego stanu.

Teraz, jeśli chodzi o zakres obowiązków: Twój kod powinien oczekiwać, że reszta bazy kodu zostanie zaimplementowana zgodnie ze specyfikacją, ale powinna być również przygotowana do obsługi z wdziękiem, gdy reszta się psuje. Nie jesteś odpowiedzialny za testowanie inny kod niż własną rękę, ale ty jesteś odpowiedzialny za swój kod odporne na niewłaściwie kod na drugim końcu, a ty jesteś odpowiedzialny za testowanie odporności na kodzie. Tak właśnie działa trzeci test powyżej.

tdammers
źródło
czy przeczytałeś komentarze pod pytaniem? OP napisał „testy”, ale miał to na myśli w sensie „kontroli walidacji” i / lub „obsługi wyjątków”
Doc Brown
1
@tdammers: przepraszam za nieporozumienie, miałem na myśli w rzeczywistości obsługę wyjątków. W każdym razie dzięki za pełną odpowiedź, ostatni akapit był tym, czego szukałem.
rdurand
1

Istnieją 3 główne zasady, które staram się kodować:

  • SUCHY

  • POCAŁUNEK

  • YAGNI

Pominięcie tych wszystkich polega na tym, że ryzykujesz pisanie kodu weryfikacyjnego, który jest powielany w innym miejscu. Jeśli reguły sprawdzania poprawności ulegną zmianie, należy je zaktualizować w wielu miejscach.

Oczywiście, w pewnym momencie w przyszłości, to może replatform bazy danych (zdarza się), w którym to przypadku można by pomyśleć o kod w więcej niż jednym miejscu byłoby korzystne. Ale ... piszesz coś, co może się nie wydarzyć.

Każdy dodatkowy kod (nawet jeśli nigdy się nie zmienia) jest narzutem, ponieważ będzie musiał zostać napisany, odczytany, zapisany i przetestowany.

Wszystkie powyższe informacje są prawdą, dlatego niedopuszczalne byłoby, abyś nie dokonywał żadnej weryfikacji. Aby wyświetlić pełną nazwę w aplikacji, potrzebujesz podstawowych danych - nawet jeśli nie zweryfikujesz samych danych.

Robbie Dee
źródło
1

Słowami laika.

Nie ma czegoś takiego jak „baza danych” lub „aplikacja” .

  1. Z bazy danych może korzystać więcej niż jedna aplikacja.
  2. Aplikacja może korzystać z więcej niż jednej bazy danych.
  3. Model bazy danych powinien wymuszać integralność danych, co obejmuje zgłaszanie błędu, gdy wymagane pole nie jest uwzględnione w operacji wstawiania, chyba że wartość domyślna jest zdefiniowana w definicji tabeli. Tę czynność należy wykonać nawet po wstawieniu wiersza bezpośrednio do bazy danych z pominięciem aplikacji. Pozwól, aby system bazy danych zrobił to za Ciebie.
  4. Bazy danych powinny chronić integralność danych i zgłaszać błędy .
  5. Logika biznesowa musi wychwytywać te błędy i zgłaszać wyjątki do warstwy prezentacji.
  6. Warstwa prezentacji musi zatwierdzać dane wejściowe, obsługiwać wyjątki lub pokazywać użytkownikowi smutnego chomika.

Jeszcze raz:

  • Baza danych-> wyrzucaj błędy
  • Logika biznesowa-> wyłapuj błędy i zgłaszaj wyjątki
  • Warstwa prezentacji-> sprawdzaj poprawność, zgłaszaj wyjątki lub wyświetlaj smutne wiadomości.
Tulains Córdova
źródło