Non posso indicare una buona risorsa online (gli articoli di Wikipedia in inglese su questi argomenti tendono ad essere migliorabili), ma posso riassumere una lezione che ho ascoltato che riguardava anche la teoria dei test di base.
Modalità di test
Esistono diverse classi di test, come test unitari o test di integrazione . Un test unitario afferma che un pezzo coerente di codice (funzione, classe, modulo) preso sui propri lavori come previsto, mentre un test di integrazione afferma che più di questi pezzi funzionano correttamente insieme.
Un caso di test è un ambiente noto in cui viene eseguito un pezzo di codice, ad esempio utilizzando input di test specifici o deridendo altre classi. Il comportamento del codice viene quindi confrontato con il comportamento previsto, ad esempio un valore di ritorno specifico.
Un test può solo dimostrare la presenza di un bug, mai l'assenza di tutti i bug. I test pongono un limite superiore alla correttezza del programma.
Copertura del codice
Per definire le metriche di copertura del codice, il codice sorgente può essere tradotto in un diagramma di flusso di controllo in cui ciascun nodo contiene un segmento lineare del codice. Il controllo scorre tra questi nodi solo alla fine di ciascun blocco ed è sempre condizionale (se condizione, quindi vai al nodo A, altrimenti vai al nodo B). Il grafico ha un nodo iniziale e un nodo finale.
- Con questo grafico, la copertura delle istruzioni è il rapporto tra tutti i nodi visitati e tutti i nodi. La copertura completa delle dichiarazioni non è sufficiente per test approfonditi.
- La copertura del ramo è il rapporto tra tutti i bordi visitati tra i nodi nel CFG e tutti i bordi. Ciò verifica insufficientemente i loop.
- La copertura del percorso è il rapporto tra tutti i percorsi visitati e tutti i percorsi, in cui un percorso è qualsiasi sequenza di bordi dall'inizio al nodo finale. Il problema è che con i loop può esserci un numero infinito di percorsi, quindi la copertura completa dei percorsi non può essere testata praticamente.
È quindi spesso utile verificare la copertura delle condizioni .
- Nella copertura delle condizioni semplici , ogni condizione atomica è una volta vera e una volta falsa, ma ciò non garantisce una copertura completa delle dichiarazioni.
- Nella copertura a più condizioni , le condizioni atomiche hanno assunto tutte le combinazioni di
true
e false
. Ciò implica una copertura completa delle filiali, ma è piuttosto costoso. Il programma potrebbe avere ulteriori vincoli che escludono determinate combinazioni. Questa tecnica è utile per ottenere la copertura delle filiali, può trovare il codice morto, ma non riesce a trovare i bug derivanti da una condizione errata .
- Nella copertura Minima condizione multipla , ogni condizione atomica e composita è una volta vera e falsa. Implica ancora la copertura completa del ramo. È un sottoinsieme di copertura a più condizioni, ma richiede meno casi di test.
Quando si costruisce un input di test utilizzando la copertura delle condizioni, è necessario prendere in considerazione il corto circuito. Per esempio,
function foo(A, B) {
if (A && B) x()
else y()
}
deve essere testato con foo(false, whatever)
, foo(true, false)
e foo(true, true)
per una copertura minima a condizioni multiple.
Se si dispone di oggetti che possono trovarsi in più stati, testare tutte le transizioni di stato analoghi ai flussi di controllo sembra ragionevole.
Esistono metriche di copertura più complesse, ma sono generalmente simili alle metriche presentate qui.
Questi sono metodi di test in white box e possono essere parzialmente automatizzati. Si noti che una suite di test di unità dovrebbe mirare ad avere una elevata copertura del codice da qualsiasi scelta metrica, ma al 100% non è sempre possibile. È particolarmente difficile testare la gestione delle eccezioni, in cui i guasti devono essere iniettati in posizioni specifiche.
Test funzionali
Quindi ci sono test funzionali che affermano che il codice aderisce alle specifiche visualizzando l'implementazione come una scatola nera. Tali test sono utili sia per i test unitari sia per i test di integrazione. Poiché è impossibile testare con tutti i possibili dati di input (ad esempio testare la lunghezza della stringa con tutte le possibili stringhe), è utile raggruppare l'input (e l'output) in classi equivalenti - se length("foo")
è corretto, foo("bar")
è probabile che funzioni anche. Per ogni possibile combinazione tra classi di equivalenza di input e output, viene scelto e testato almeno un input rappresentativo.
Uno dovrebbe inoltre testare
- casi limite
length("")
, foo("x")
, length(longer_than_INT_MAX)
,
- valori consentiti dalla lingua, ma non dal contratto della funzione
length(null)
, e
- possibili dati spazzatura
length("null byte in \x00 the middle")
...
Con i valori numerici, ciò significa test 0, ±1, ±x, MAX, MIN, ±∞, NaN
e con confronti in virgola mobile test due float vicini. Come ulteriore aggiunta, i valori di test casuali possono essere scelti dalle classi di equivalenza. Per facilitare il debug, vale la pena registrare il seme utilizzato ...
Test non funzionali: test di carico, test di stress
Un software ha requisiti non funzionali, che devono anche essere testati. Questi includono test ai limiti definiti (test di carico) e oltre (test di stress). Per un gioco per computer, ciò potrebbe far valere un numero minimo di fotogrammi al secondo in un test di carico. Un sito Web può essere sottoposto a stress test per osservare i tempi di risposta quando il doppio dei visitatori previsti colpisce i server. Tali test non sono rilevanti solo per interi sistemi ma anche per singole entità: in che modo una tabella hash si degrada con un milione di voci?
Altri tipi di test sono test dell'intero sistema in cui vengono simulati scenari o test di accettazione per dimostrare che il contratto di sviluppo è stato rispettato.
Metodi non di prova
Recensioni
Esistono tecniche non sperimentali che possono essere utilizzate per la garanzia della qualità. Esempi sono procedure dettagliate, revisioni del codice formali o programmazione di coppie. Mentre alcune parti possono essere automatizzate (ad es. Utilizzando linters), in genere richiedono molto tempo. Tuttavia, le revisioni del codice da parte di programmatori esperti hanno un alto tasso di individuazione dei bug e sono particolarmente utili durante la progettazione, dove non è possibile eseguire test automatici.
Quando le revisioni del codice sono così eccezionali, perché scriviamo ancora dei test? Il grande vantaggio delle suite di test è che possono essere eseguite (principalmente) automaticamente e sono quindi molto utili per i test di regressione .
Verifica formale
La verifica formale va e dimostra certe proprietà del codice. La verifica manuale è principalmente praticabile per le parti critiche, tanto meno per interi programmi. Le prove mettono un limite inferiore alla correttezza del programma. Le prove possono essere automatizzate in una certa misura, ad esempio tramite un controllo statico del tipo.
Alcuni invarianti possono essere controllati esplicitamente usando le assert
dichiarazioni.
Tutte queste tecniche hanno il loro posto e sono complementari. TDD scrive i test funzionali in anticipo, ma i test possono essere valutati in base alle loro metriche di copertura una volta implementato il codice.
Scrivere codice testabile significa scrivere piccole unità di codice che possono essere testate separatamente (funzioni di supporto con granularità adeguata, principio di responsabilità singola). Meno argomenti prende ogni funzione, meglio è. Tale codice si presta anche per l'inserimento di oggetti finti, ad esempio tramite iniezione di dipendenza.
double pihole(double value) { return (value - Math.PI) / (value - Math.PI); }
che ho imparato dal mio insegnante di matematica . Questo codice ha esattamente un foro , che non può essere scoperto automaticamente dai soli test della scatola nera. In matematica non esiste questo buco. Nel calcolo puoi chiudere il buco se i limiti unilaterali sono uguali.