La copertura del percorso garantisce la ricerca di tutti i bug?


64

Se viene testato ogni percorso attraverso un programma, ciò garantisce la ricerca di tutti i bug?

In caso contrario, perché no? Come hai potuto passare attraverso ogni possibile combinazione del flusso del programma e non trovare il problema se ne esiste uno?

Esito a suggerire che "tutti i bug" possono essere trovati, ma forse è perché la copertura del percorso non è pratica (in quanto è combinatoria) quindi non è mai stata sperimentata?

Nota: questo articolo fornisce un breve riepilogo dei tipi di copertura mentre ci penso.


33
Ciò equivale al problema di arresto .

31
E se il codice che avrebbe dovuto essere lì, non lo è?
Remco Gerlich,

6
@Snowman: No, non lo è. Non è possibile risolvere il problema di arresto per tutti i programmi ma per molti programmi specifici è risolvibile. Per questi programmi, tutti i percorsi di codice possono essere enumerati in un tempo limitato (sebbene possibilmente lungo).
Jørgen Fogh,

3
@ JørgenFogh Ma quando si cerca di trovare bug in qualsiasi programma, non è a priori sconosciuto se il programma si interrompe o no? Non è questa domanda sul metodo generale di "trovare tutti i bug in qualsiasi programma tramite la copertura del percorso"? In tal caso, non è simile a "scoprire se un programma si ferma"?
Andres F.

1
@AndresF. non si sa se il programma si arresta se il sottoinsieme della lingua in cui è scritto è in grado di esprimere un programma senza interruzioni. Se il tuo programma è scritto in C senza usare loop / ricorsione / setjmp senza limiti, ecc., O in Coq, o in ESSL, allora deve fermarsi e tutti i percorsi possono essere tracciati. (La completezza di Turing è gravemente sopravvalutata)
Leushenko,

Risposte:


128

Se viene testato ogni percorso attraverso un programma, ciò garantisce la ricerca di tutti i bug?

No

In caso contrario, perché no? Come hai potuto passare attraverso ogni possibile combinazione del flusso del programma e non trovare il problema se ne esiste uno?

Perché anche se testate tutti i possibili percorsi , non li avete ancora testati con tutti i possibili valori o tutte le possibili combinazioni di valori . Ad esempio (pseudocodice):

def Add(x as Int32, y as Int32) as Int32:
   return x + y

Test.Assert(Add(2, 2) == 4) //100% test coverage
Add(MAXINT, 5) //Throws an exception, despite 100% test coverage

Sono ormai passati due decenni da quando è stato sottolineato che i test del programma possono dimostrare in modo convincente la presenza di bug, ma non possono mai dimostrare la loro assenza. Dopo aver citato devotamente questa osservazione ben pubblicizzata, l'ingegnere del software ritorna all'ordine del giorno e continua a perfezionare le sue strategie di test, proprio come l'alchimista di un tempo, che ha continuato a perfezionare le sue purificazioni crisocosmiche.

- EW Dijkstra (Emphasis aggiunto. Scritto nel 1988. Sono passati considerevolmente più di 2 decenni.)


7
@digitgopher: suppongo, ma se un programma non ha input, quale cosa utile fa?
Mason Wheeler,

34
C'è anche la possibilità di perdere test di integrazione, bug nei test, bug nelle dipendenze, bug nel sistema di compilazione / distribuzione o bug nelle specifiche / requisiti originali. Non puoi mai garantire di trovare tutti i bug.
Ixrec,

11
@Ixrec: SQLite fa uno sforzo piuttosto coraggioso, però! Ma guarda che enorme sforzo è! Ciò non si adatterebbe bene a grandi codebase.
Mason Wheeler,

13
Non solo non avresti testato tutti i possibili valori o combinazioni di questi, non hai testato tutti i tempi relativi, alcuni dei quali potrebbero esporre le condizioni di gara o addirittura rendere il tuo test in un punto morto, il che farebbe in modo che non si riferisse nulla . Non sarebbe nemmeno un fallimento!
Iwillnotexist Idonotexist del

14
Il mio ricordo (rafforzato da scritti come questo ) è che Dijkstra credeva che nelle buone pratiche di programmazione, la prova che un programma è corretto (in tutte le condizioni) dovrebbe essere in primo luogo parte integrante dello sviluppo del programma. Visto da quel punto di vista, il test è come l'alchimia. Piuttosto che iperbole, penso che questa fosse un'opinione molto forte espressa in un linguaggio molto forte.
David K,

71

Oltre alla risposta di Mason , c'è anche un altro problema: la copertura non ti dice quale codice è stato testato, ti dice quale codice è stato eseguito .

Immagina di avere una suite di test con una copertura del percorso al 100%. Ora rimuovi tutte le asserzioni ed esegui di nuovo il testuite. Voilà, la suite di test ha ancora una copertura del percorso del 100%, ma non verifica assolutamente nulla.


2
Potrebbe assicurarsi che non vi siano eccezioni quando si chiama il codice testato (con i parametri nel test). Questo è leggermente più che niente.
Paŭlo Ebermann,

7
@ PaŭloEbermann D'accordo, leggermente più che niente. Tuttavia, è tremendamente meno di "trovare tutti i bug";)
Andres F.

1
@ PaŭloEbermann: le eccezioni sono un percorso di codice. Se il codice potrebbe essere lanciato ma con determinati dati del test non viene eseguito, il test non raggiunge il 100% di copertura del percorso. Questo non è specifico delle eccezioni come meccanismo di gestione degli errori. Visual Basic di ON ERROR GOTOè anche un percorso, come è C di if(errno).
Salterio

1
@MSalters Sto parlando di codice che (per specifica) non dovrebbe generare alcuna eccezione, indipendentemente dall'input. Se ne lancia una, sarebbe un bug. Naturalmente, se si dispone di codice specificato per generare un'eccezione, è necessario verificarlo. (E ovviamente, come ha detto Jörg, il solo fatto di verificare che il codice non generi un'eccezione di solito non è sufficiente per assicurarsi che faccia la cosa giusta, anche per il codice non-lancio.) E alcune eccezioni possono essere generate da un non percorso del codice visibile, come per la dereferenza del puntatore null o la divisione per zero. Il tuo strumento di copertura del percorso li cattura?
Paŭlo Ebermann,

2
Questa risposta lo inchioda. Vorrei prendere ulteriormente la richiesta e dire che, a causa di ciò, la copertura del percorso non garantisce mai di trovare nemmeno un singolo bug. Ci sono metriche che possono garantire almeno che vengano rilevate modifiche, tuttavia - test mutazione può effettivamente garantire che (alcuni) modifiche del codice saranno rilevati.
eis

34

Ecco un esempio più semplice per arrotondare le cose. Considera il seguente algoritmo di ordinamento (in Java):

int[] sort(int[] x) { return new int[] { x[0] }; }

Ora proviamo:

sort(new int[] { 0xCAFEBABE });

Ora, considera che (A) questa particolare chiamata per sortrestituire il risultato corretto, (B) tutti i percorsi di codice sono stati coperti da questo test.

Ma, ovviamente, il programma in realtà non ordina.

Ne consegue che la copertura di tutti i percorsi di codice non è sufficiente a garantire che il programma non abbia bug.


12

Considera la absfunzione, che restituisce il valore assoluto di un numero. Ecco un test (Python, immagina un framework di test):

def test_abs_of_neg_number_returns_positive():
    assert abs(-3) == 3

Questa implementazione è corretta, ma ottiene solo il 60% di copertura del codice:

def abs(x):
    if x < 0:
        return -x
    else:
        return x

Questa implementazione è sbagliata, ma ottiene una copertura del codice del 100%:

def abs(x):
    return -x

2
Ecco un'altra implementazione che supera il test (scusate il Python non linebroken): def abs(x): if x == -3: return 3 else: return 0è possibile eludere la else: return 0parte e ottenere una copertura del 100%, ma la funzione sarebbe sostanzialmente inutile anche se supera il test unitario.
un CVn del

7

Ancora un'altra aggiunta alla risposta di Mason , il comportamento di un programma può dipendere dall'ambiente di runtime.

Il seguente codice contiene un Use-After-Free:

int main(void)
{
    int* a = malloc(sizeof(a));
    int* b = a;
    *a = 0;
    free(a);
    *b = 12; /* UAF */
    return 0;
}

Questo codice è Undefined Behaviour, a seconda della configurazione (release | debug), del sistema operativo e del compilatore produrrà comportamenti diversi. Non solo la copertura del percorso non ti garantirà che troverai l'UAF, ma la tua suite di test in genere non coprirà i vari comportamenti possibili dell'UAF che dipendono dalla configurazione.

In un'altra nota, anche se la copertura del percorso dovesse garantire la ricerca di tutti i bug, è improbabile che possa essere realizzato in pratica su qualsiasi programma. Considera il seguente:

int main(int a, int b)
{
    if (a != b) {
        if (cryptohash(a) == cryptohash(b)) {
            return ERROR;
        }
    }
    return 0;
} 

Se la tua suite di test può generare tutti i percorsi per questo, allora congratulazioni sei un crittografo.


Facile per numeri interi sufficientemente piccoli :)
CodesInChaos

Senza sapere nulla cryptohash, è un po 'difficile dire cosa sia "sufficientemente piccolo". Forse ci vogliono due giorni per completare un supercalculatore. Ma sì, intpotrebbe rivelarsi un po ' short.
Dureuill

Con numeri interi a 32 bit e hash crittografici tipici (SHA2, SHA3, ecc.) Il calcolo dovrebbe essere abbastanza economico. Un paio di secondi circa.
Codici InCos

7

Dalle altre risposte emerge chiaramente che la copertura del codice al 100% nei test non significa la correttezza del codice al 100% o che tutti i bug rilevati dai test verranno individuati (non importa bug che nessun test potrebbe rilevare).

Un altro modo di rispondere a questa domanda è quello pratico:

Esistono, nel mondo reale, e in effetti sul proprio computer, molti software che vengono sviluppati utilizzando una serie di test che forniscono una copertura del 100% e che hanno ancora bug, inclusi bug che potrebbero essere identificati da test migliori.

Una domanda implicita pertanto è:

Qual è il punto di strumenti di copertura del codice?

Gli strumenti di copertura del codice aiutano a identificare le aree che si è trascurato di testare. Questo può andare bene (il codice è dimostrabilmente corretto anche senza test), può essere impossibile da risolvere (per qualche ragione un percorso non può essere raggiunto), oppure può essere la posizione di un grande bug puzzolente ora o in seguito a future modifiche.

In qualche modo il controllo ortografico è paragonabile: qualcosa può "passare" il controllo ortografico ed essere scritto in modo errato in modo tale da abbinare una parola nel dizionario. Oppure può "fallire" perché le parole corrette non sono nel dizionario. Oppure può passare ed essere assolutamente senza senso. Il controllo ortografico è uno strumento che ti aiuta a identificare i luoghi che potresti aver perso nella tua correzione di bozze, ma così come non può garantire una lettura di bozze completa e corretta, quindi la copertura del codice non può garantire test completi e corretti.

E, naturalmente, il modo errato di usare il controllo ortografico è notoriamente quello di seguire ogni suggerimento che il mare suggerisce, quindi la cosa dell'anatra peggiora se la pecora lo lasciava in prestito.

Con la copertura del codice può essere allettante, soprattutto se hai un 98% quasi perfetto, riempire i casi in modo che i percorsi rimanenti vengano colpiti.

Questo è l'equivalente di raddrizzare con il controllo ortografico cucire che sono tutte parole tempo o nodo sono tutte le parole appropriate. Il risultato è un pasticcio di papere.

Tuttavia, se si considera quali test hanno realmente bisogno dei percorsi non coperti, lo strumento di copertura del codice avrà fatto il suo lavoro; non promettendoti la correttezza, ma sottolineando parte del lavoro che doveva essere fatto.


+1 Mi piace questa risposta perché è costruttiva e menziona alcuni dei vantaggi della copertura.
Andres F.

4

La copertura del percorso non può dirti se tutte le funzionalità richieste sono state implementate. Lasciare una funzione è un bug, ma la copertura del percorso non la rileverà.


1
Penso che dipenda dalla definizione di un bug. Non credo che le caratteristiche o funzionalità mancanti debbano essere considerate come bug.
eis,

@eis - non vedi un problema con un prodotto la cui documentazione dice che fa X quando in realtà no? Questa è una definizione piuttosto ristretta di "bug". Quando gestivo il QA per la linea di prodotti C ++ di Borland non eravamo così generosi.
Pete Becker,

Non vedo perché la documentazione direbbe che fa X se questo non è mai stato implementato
eis

@eis - se il design originale richiedeva la funzione X la documentazione potrebbe finire per descrivere la funzione X. Se nessuno lo implementasse, questo è un bug e la copertura del percorso (o qualsiasi altro tipo di test della scatola nera) non lo troverà.
Pete Becker,

Spiacenti, la copertura del percorso è test su scatola bianca , non su scatola nera . I test su scatola bianca non possono rilevare le funzioni mancanti.
Pete Becker,

4

Parte del problema è che la copertura del 100% garantisce che il codice funzionerà correttamente solo dopo una singola esecuzione . Alcuni bug come perdite di memoria potrebbero non essere evidenti o causare problemi dopo una singola esecuzione, ma nel tempo causeranno problemi all'applicazione.

Ad esempio, supponiamo di avere un'applicazione che si collega a un database. Forse in un metodo il programmatore dimentica di chiudere la connessione al database quando ha finito con la sua query. È possibile eseguire diversi test su questo metodo e non trovare errori con la sua funzionalità, ma il server del database potrebbe essere eseguito in uno scenario in cui non è disponibile connessioni perché questo particolare metodo non ha chiuso la connessione quando è stata eseguita e le connessioni aperte devono ora timeout.


Concordato sul fatto che fa parte del problema, ma il vero problema è più fondamentale di così. Anche con un computer teorico con memoria infinita e nessuna concorrenza, la copertura del test al 100% non implica l'assenza di bug. Triviali esempi di questo abbondano nelle risposte qui, ma eccone un altro: se il mio programma è times_two(x) = x + 2, questo sarà completamente coperto dalla suite di test assert(times_two(2) == 4), ma questo è ovviamente ovviamente un codice errato! Non c'è bisogno di perdite di memoria :)
Andres F.

2
È un ottimo punto e riconosco che è un chiodo più grande / più fondamentale nella bara della possibilità di applicazioni prive di bug, ma come dici tu è già stato aggiunto qui e volevo aggiungere qualcosa che non era abbastanza coperto in risposte esistenti. Ho sentito di applicazioni che si sono arrestate in modo anomalo perché le connessioni al database non sono state rilasciate nuovamente nel pool di connessioni quando non erano più necessarie - Una perdita di memoria è solo un esempio canonico di cattiva gestione delle risorse. Il mio punto era aggiungere che una corretta gestione delle risorse in generale non può essere interamente testata.
Derek W,

Buon punto. Concordato.
Andres F.

3

Se viene testato ogni percorso attraverso un programma, ciò garantisce la ricerca di tutti i bug?

Come già detto, la risposta è NO.

In caso contrario, perché no?

Oltre a ciò che viene detto, ci sono bug che appaiono a diversi livelli, che non possono essere testati con test unitari. Solo per citarne alcuni:

  • bug rilevati con i test di integrazione (dopo tutto i test unitari non dovrebbero usare risorse reali)
  • bug nei requisiti
  • bug nel design e nell'architettura

2

Cosa significa testare ogni percorso?

Le altre risposte sono ottime, ma voglio solo aggiungere che la condizione "ogni percorso attraverso un programma è testato" è di per sé vaga.

Considera questo metodo:

def add(num1, num2)
  foo = "bar"  # useless statement
  $global += 1 # side effect
  num1 + num2  # actual work
end

Se scrivi un test che afferma add(1, 2) == 3, uno strumento di copertura del codice ti dirà che ogni riga è esercitata. Ma in realtà non hai affermato nulla sull'effetto collaterale globale o sull'incarico inutile. Quelle linee sono state eseguite, ma non sono state davvero testate.

I test di mutazione aiuterebbero a trovare problemi come questo. Uno strumento di test di mutazione avrebbe un elenco di modi predeterminati per "mutare" il codice e vedere se i test continuano a passare. Per esempio:

  • Una mutazione potrebbe cambiare +=in -=. Tale mutazione non provocherebbe un fallimento del test, quindi dimostrerebbe che il test non afferma nulla di significativo sull'effetto collaterale globale.
  • Un'altra mutazione potrebbe eliminare la prima riga. Tale mutazione non provocherebbe un fallimento del test, quindi proverebbe che il test non afferma nulla di significativo sull'assegnazione.
  • Ancora un'altra mutazione potrebbe eliminare la terza riga. Ciò provocherebbe un fallimento del test, che in questo caso mostra che il tuo test fa valere qualcosa su quella linea.

In sostanza, i test di mutazione sono un modo per testare i tuoi test . Ma proprio come non testerai mai la funzione effettiva con ogni possibile set di input, non eseguirai mai ogni possibile mutazione, quindi di nuovo, questo è limitato.

Ogni test che possiamo fare è euristico per passare a programmi senza bug. Niente è perfetto.


0

Bene ... , in realtà, se ogni percorso "attraverso" il programma viene testato. Ciò significa che ogni possibile percorso attraverso l'intero spazio di tutti i possibili stati che il programma può avere, comprese tutte le variabili. Anche per un programma compilato staticamente molto semplice - diciamo, un vecchio cruncher numerico Fortran - questo non è fattibile, anche se può almeno essere immaginabile: se hai solo due variabili intere, in pratica hai a che fare con tutti i modi possibili per collegare punti su una griglia bidimensionale; in realtà assomiglia molto al commesso viaggiatore. Per n tali variabili, hai a che fare con uno spazio n- dimensionale, quindi per qualsiasi programma reale, l'attività è completamente irrintracciabile.

Peggio: per cose serie, si ha non solo un numero fisso di variabili primitive, ma creare le variabili al volo in chiamate di funzione, o avere variabili di dimensione variabile ... o qualcosa di simile, per quanto possibile in un linguaggio Turing-completo. Ciò rende lo spazio dello stato infinito-dimensionale, mandando in frantumi tutte le speranze di piena copertura, anche con apparecchiature di test assurdamente potenti.


Detto questo ... in realtà le cose non sono così cupe. Si è possibile proove interi programmi siano corrette, ma dovrete rinunciare a un paio di idee.

Primo: è consigliabile passare a una lingua dichiarativa. I linguaggi imperativi, per qualche ragione, sono sempre stati i più popolari, ma il modo in cui mescolano algoritmi e interazioni del mondo reale rende estremamente difficile persino dire cosa intendi con "corretto".

Molto più facile nei linguaggi di programmazione puramente funzionali : questi hanno una chiara distinzione tra le proprietà veramente interessanti delle funzioni matematiche e le interazioni fuzzy del mondo reale di cui non si può davvero dire nulla. Per le funzioni, è molto facile specificare il "comportamento corretto": se per tutti i possibili input (dai tipi di argomento) viene visualizzato il risultato desiderato corrispondente, la funzione si comporta correttamente.

Ora, dici che è ancora intrattabile ... dopo tutto, lo spazio di tutti i possibili argomenti è in generale anche di dimensione infinita. È vero, anche se per una singola funzione, anche un test di copertura ingenuo ti porta molto più lontano di quanto tu possa mai sperare in un programma imperativo! Tuttavia, esiste un incredibile strumento potente che cambia il gioco: quantificazione universale / polimorfismo parametrico . Fondamentalmente, questo ti consente di scrivere funzioni su tipi di dati molto generali, con la garanzia che se funziona per un semplice esempio dei dati, funzionerà per qualsiasi possibile input.

Almeno teoricamente. Non è facile trovare i tipi giusti che sono davvero così generali che puoi provarlo completamente - di solito, hai bisogno di un linguaggio tipizzato in modo dipendente , e questi tendono ad essere piuttosto difficili da usare. Ma scrivere in uno stile funzionale con il solo polimorfismo parametrico aumenta già in modo enfatico il tuo "livello di sicurezza": non troverai necessariamente tutti i bug, ma dovrai nasconderli abbastanza bene in modo che il compilatore non li veda!


Non sono d'accordo con la tua prima frase. Passare attraverso ogni stato del programma, di per sé, non rileva alcun bug. Anche se controlli eventuali arresti anomali ed errori espliciti, non hai ancora verificato la funzionalità effettiva in alcun modo, quindi hai coperto solo una piccola parte dello spazio degli errori.
Matteo Leggi il

@MatthewRead: se lo si applica di conseguenza, lo "spazio errore" è un sottospazio corretto dello spazio di tutti gli stati. Ovviamente è ipotetico perché anche gli stati “corretti” costituiscono uno spazio troppo grande per consentire test esaustivi.
leftaroundabout
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.