Come migliorare nel test del proprio codice


45

Sono uno sviluppatore di software relativamente nuovo e una delle cose che penso che dovrei migliorare è la mia capacità di testare il mio codice. Ogni volta che sviluppo una nuova funzionalità, trovo davvero difficile seguire tutti i percorsi possibili in modo da poter trovare i bug. Tendo a seguire il percorso in cui tutto funziona. So che questo è un problema ben noto che i programmatori hanno, ma non abbiamo tester al mio attuale datore di lavoro e i miei colleghi sembrano essere abbastanza bravi in ​​questo.

Nella mia organizzazione, non eseguiamo né test-driven development né unit test. Mi aiuterebbe molto, ma non è probabile che questo cambi.

Cosa pensate che potrei fare io per superare questo? Quale approccio usi quando collaudi il tuo codice?


28
Solo perché la tua organizzazione non utilizza TDD o test di unità non significa che non puoi, purché continui a rispettare le scadenze e produca un codice di qualità.
Thomas Owens

1
Immagino che Thomas mi abbia battuto, ma sono in una situazione simile. Scrivo aspettative di altissimo livello su cosa dovrebbe fare un cambio di codice e scrivo unit test se e quando posso (anche se la nostra azienda non effettua ufficialmente test unitari). Non devi impegnarli, e sono ottimi modi per imparare come dovrebbero agire le funzioni (o dovrebbero agire dopo averle riparate).
Brian,

6
@Brian, penso che dovresti impegnarli indipendentemente dal fatto che altri li utilizzino attualmente. Forse mostrare buone pratiche porterà altri a seguirlo.
CaffGeek,

Risposte:


20

Il compito di un programmatore è costruire cose.

Il compito di un tester è quello di rompere le cose.

Il più difficile è rompere le cose che hai appena costruito. Ci riuscirai solo superando questa barriera psicologica.


27
-1 Il compito di un programmatore è quello di costruire cose che funzionino . Ciò comporta sempre un certo numero di test. Concordo sulla necessità di un ruolo di tester separato, ma non è l'unica linea di difesa.
Matthew Rodatus,

8
@Matthew Rodatus - La quantità di test coinvolti sul lato programmatore mira solo a verificare che ciò che dovrebbe funzionare effettivamente funzioni. Per quanto riguarda i tester, l'obiettivo è trovare bug, non osservare che il codice funziona.
mouviciel,

2
È diverso dalla tua risposta, e sono d'accordo con questo. Ma non sono ancora pienamente d'accordo. La scrittura del codice di qualità arriva attraverso la pratica mentre impari a pensare, in anticipo, alle possibilità di fallimento. Non impari a pensare attraverso le possibilità di fallimento senza imparare a creare quelle possibilità. Non penso che i programmatori debbano essere l'unica linea di difesa, ma dovrebbero essere la prima linea di difesa. La questione in gioco è quella della completezza e padronanza del mestiere.
Matthew Rodatus,

2
@mouviciel - falsa dicotomia. Il compito di un programmatore è quello di costruire cose che funzionino, e lo fa pensando a priori in quali condizioni dovrebbe funzionare il suo codice. E questo è verificato, almeno, creando test distruttivi + alcune analisi al contorno ad hoc (di nuovo, almeno .) Inoltre, un buon programmatore funziona contro le specifiche e le specifiche (quando valide) sono sempre testabili. Quindi un buon programmatore sviluppa test per verificare che questi requisiti siano soddisfatti nel codice (e di solito lo fai scrivendo test di richiesta che inizialmente falliscono fino a quando non hai codice che supera i test.)
luis.espinal

2
@Dave Lasley - Questo è esattamente il mio punto: l'architetto non è la persona migliore per abbattere la sua casa: è troppo orgoglioso di quanto sia forte per poterne vedere i difetti. Solo un altro architetto (non il tizio fuori strada) può portare un occhio obiettivo sulla casa e scoprire che la casa potrebbe rompersi in alcune condizioni specifiche che l'ex architetto era troppo cieco per immaginare.
mouviciel,

14

Franciso, farò alcune ipotesi qui, sulla base di ciò che hai detto:

"Non eseguiamo né TDD né test unitari. Mi aiuterebbe molto, ma non è probabile che questo cambierà."

Da questo, sospetto che il tuo team non attribuisca molto valore ai test o alla gestione non disporrà di tempo per il team per provare a riordinare il codice esistente e ridurre al minimo il debito tecnico .

In primo luogo, è necessario convincere il team / la direzione del valore dei test. Sii diplomatico. Se la gestione sta facendo avanzare il tuo team, devi mostrare loro alcuni fatti, come il tasso di difetto per ogni versione. Il tempo impiegato per correggere i difetti potrebbe essere speso meglio in altre cose, come migliorare l'applicazione e renderla più adattabile alle esigenze future.

Se il team e la direzione in generale sono apatici riguardo alla correzione del codice e non ti senti soddisfatto, potrebbe essere necessario cercare un altro posto di lavoro, a meno che tu non riesca a convincerli come ho già detto. Ho riscontrato questo problema in diversa misura in tutti i posti in cui ho lavorato. Potrebbe essere qualsiasi cosa, dalla mancanza di un modello di dominio adeguato, alla scarsa comunicazione nel team.

Prendersi cura del proprio codice e della qualità del prodotto che si sviluppa è un buon attributo e uno che si desidera sempre incoraggiare nelle altre persone.


11

Se si codifica in C, Objective-C o C ++, è possibile utilizzare l' analizzatore statico CLang per criticare la fonte senza eseguirla effettivamente.

Sono disponibili alcuni strumenti di debug della memoria: ValGrind, Guard Malloc su Mac OS X, Electric Fence su * NIX.

Alcuni ambienti di sviluppo offrono la possibilità di utilizzare un allocatore di memoria di debug, che fa cose come riempire di immondizia le pagine appena allocate e le pagine appena liberate, rilevare la liberazione di puntatori non allocati e scrivere alcuni dati prima e dopo ogni blocco di heap, essendo il debugger chiamato se il modello noto di tali dati cambia mai.

Qualcuno su Slashdot ha detto di aver ricavato molto valore dalla linea di sorgente sempre nuova a un singolo debugger. "Questo è tutto", ha detto. Non seguo sempre il suo consiglio, ma quando l'ho fatto mi è stato molto utile. Anche se non hai un caso di test che stimola un percorso di codice non comune, puoi modificare una variabile nel tuo debugger per prendere tali percorsi, ad esempio allocando un po 'di memoria, quindi utilizzando il debugger per impostare il tuo nuovo puntatore su NULL invece che sul indirizzo di memoria, quindi passare attraverso il gestore errori allocazione.

Usa asserzioni: la macro assert () in C, C ++ e Objective-C. Se la tua lingua non fornisce una funzione di assert, scrivine una tu.

Usa assert liberamente, quindi lasciali nel tuo codice. Chiamo assert () "Il test che continua a testare". Li uso più comunemente per verificare i presupposti nel punto di ingresso della maggior parte delle mie funzioni. Questa è una parte della "Programmazione per contratto", che è integrata nel linguaggio di programmazione Eiffel. L'altra parte sono le postcondizioni, ovvero l'uso di assert () nei punti di ritorno della funzione, ma trovo che non riesco a ottenere così tanto chilometraggio rispetto alle condizioni preliminari.

Puoi anche usare assert per controllare gli invarianti di classe. Sebbene nessuna classe sia strettamente richiesta per avere alcun invariante, le classi progettate in modo più ragionevole li hanno. Un invariante di classe è una condizione che è sempre vera diversa dall'interno delle funzioni membro che potrebbe posizionare temporaneamente l'oggetto in uno stato incoerente. Tali funzioni devono sempre ripristinare la coerenza prima di tornare.

Pertanto ogni funzione membro può controllare l'invariante all'entrata e all'uscita e la classe può definire una funzione chiamata CheckInvariant che qualsiasi altro codice può chiamare in qualsiasi momento.

Utilizza uno strumento di copertura del codice per verificare quali linee della tua sorgente vengono effettivamente testate, quindi progetta test che stimolino le linee non testate. Ad esempio, è possibile controllare i gestori di memoria insufficiente eseguendo l'app all'interno di una macchina virtuale configurata con poca memoria fisica e senza file di scambio o molto piccola.

(Per qualche motivo non sono mai stato al corrente, mentre BeOS poteva funzionare senza un file di scambio, era altamente instabile in quel modo. Dominic Giampaolo, che ha scritto il filesystem BFS, mi ha esortato a non eseguire BeOS senza scambio. vedi perché dovrebbe importare, ma deve essere stato una sorta di artefatto dell'implementazione.)

Dovresti anche testare la risposta del tuo codice agli errori I / O. Prova a archiviare tutti i tuoi file su una condivisione di rete, quindi disconnetti il ​​cavo di rete mentre l'app ha un carico di lavoro elevato. Allo stesso modo, scollegare il cavo - o spegnere il wireless - se si sta comunicando su una rete.

Una cosa che trovo particolarmente esasperante sono i siti Web che non dispongono di un codice Javascript affidabile. Le pagine di Facebook caricano dozzine di piccoli file Javascript, ma se qualcuno di loro non riesce a scaricare, l'intera pagina si rompe. Ci deve essere solo un modo per fornire un po 'di tolleranza agli errori, ad esempio tentando un download, o per fornire un qualche tipo di fallback ragionevole quando alcuni degli script non sono stati scaricati.

Prova a uccidere la tua app con il debugger o con "kill -9" su * NIX mentre è nel bel mezzo della scrittura di un file grande e importante. Se la tua app è ben progettata, l'intero file verrà scritto o non verrà affatto scritto, o forse se è solo parzialmente scritto, ciò che viene scritto non verrà danneggiato, con quali dati salvati saranno completamente utilizzabili da l'app dopo aver riletto il file.

i database hanno sempre I / O su disco a tolleranza d'errore, ma quasi nessun altro tipo di app lo fa. Mentre i filesystem con journaling prevengono la corruzione del filesystem in caso di blackout o crash, non fanno nulla per prevenire il danneggiamento o la perdita dei dati dell'utente finale. Questa è la responsabilità delle applicazioni utente, ma quasi nessun altro oltre ai database implementa la tolleranza agli errori.


1
+1 un sacco di consigli pratici che non hanno bisogno del supporto di nessun altro. L'unica cosa che aggiungerei è che asserire è per documentare e verificare condizioni che non possono fallire a meno che non ci sia un bug nel codice . Non affermare mai cose che potrebbero fallire a causa della "sfortuna", come un file essenziale non trovato, o input non valido, ecc.
Ian Goldby,

10

Quando guardo testare il mio codice, di solito passo attraverso una serie di processi di pensiero:

  1. Come divido questo "oggetto" in pezzi di dimensioni verificabili? Come posso isolare solo ciò che voglio testare? Quali matrici / beffe dovrei creare?
  2. Per ogni blocco: come posso testare questo blocco per assicurarmi che risponda correttamente a una serie ragionevole di input corretti?
  3. Per ogni blocco: come testare che il blocco risponda correttamente a input errati (puntatori NULL, valori non validi)?
  4. Come testare i limiti (ad es. Dove i valori vanno da con segno a senza segno, da 8 bit a 16 bit, ecc.)?
  5. In che misura i miei test coprono il codice? Ci sono delle condizioni che ho perso? [Questo è un ottimo posto per gli strumenti di copertura del codice.] Se c'è un codice che è stato perso e non può mai essere eseguito, deve davvero essere lì? [Questa è un'altra domanda!]

Il modo più semplice che ho trovato per fare questo è sviluppare i miei test insieme al mio codice. Non appena ho scritto anche un frammento di codice, mi piace scrivere un test per questo. Cercare di eseguire tutti i test dopo aver codificato diverse migliaia di righe di codice con complessità del codice ciclomatico non banale è un incubo. Aggiungere uno o due ulteriori test dopo aver aggiunto alcune righe di codice è davvero semplice.

A proposito, solo perché la società in cui lavori e / oi tuoi colleghi non eseguono Unit Testing o TDD, non significa che non puoi provarli, a meno che non siano specificamente vietati. Forse usarli per creare codice robusto sarà un buon esempio per gli altri.


5

Oltre ai consigli forniti nelle altre risposte, suggerirei di utilizzare strumenti di analisi statica (Wikipedia ha un elenco di una serie di strumenti di analisi statica per varie lingue ) per trovare potenziali difetti prima dell'inizio dei test e monitorare alcune metriche correlate la testabilità del codice, come la complessità ciclomatica , le misure della complessità di Halstead , la coesione e l'accoppiamento (è possibile misurarle con fan-in e fan-out).

Trovare un codice difficile da testare e semplificare il test renderà più semplice la scrittura di casi di test. Inoltre, la cattura precoce dei difetti aggiungerà valore a tutte le vostre pratiche di garanzia della qualità (che includono i test). Da qui, familiarizzare con gli strumenti di test delle unità e gli strumenti di derisione renderà più semplice l'implementazione dei test.


1
+1 per complessità ciclomatica. "Trovo davvero difficile seguire tutti i percorsi possibili in modo da poter trovare i bug" implica per me che potrebbe essere necessario suddividere il codice del PO in blocchi più piccoli e meno complessi.
Toby,

@Toby Sì, è per questo che ho deciso di gettare un'analisi statica. Se non riesci a provare il tuo codice, hai problemi. E se hai un problema con il tuo codice, potrebbero essercene altri. Utilizzare uno strumento per trovare potenziali flag di avviso, valutarli e correggerli secondo necessità. Non solo avrai più codice testabile, ma anche più codice leggibile.
Thomas Owens

3

Puoi esaminare il possibile utilizzo delle tabelle di verità per aiutarti a definire tutti i potenziali percorsi nel tuo codice. È impossibile tenere conto di tutte le possibilità in funzioni complesse, ma una volta stabilita la gestione per tutti i percorsi noti, è possibile stabilire una gestione per il caso else.

Tuttavia, la maggior parte di questa particolare abilità viene appresa dall'esperienza. Dopo aver utilizzato un determinato framework per un periodo di tempo significativo, inizi a vedere gli schemi e le caratteristiche del comportamento che ti permetteranno di guardare un pezzo di codice e vedere dove una piccola modifica potrebbe causare un errore grave. L'unico modo in cui riesco a pensare di aumentare la tua attitudine in questo è la pratica.


3

Se, come hai detto, non hai bisogno di test unitari, non vedo alcun approccio migliore rispetto al tentativo di rompere manualmente il tuo codice.

Prova a spingere il tuo codice al limite . Ad esempio, provare a passare le variabili a una funzione che supera i limiti del limite. Hai una funzione che dovrebbe filtrare l'input dell'utente? Prova a inserire diverse combinazioni di caratteri.

Considera il punto di vista dell'utente . Cerca di essere uno degli utenti che utilizzerà l'applicazione o la libreria di funzioni.


1
+1 per menzionare vedere le cose dal punto di vista dell'utente.
Matthew Rodatus,

3

ma non abbiamo tester presso il mio attuale datore di lavoro e i miei colleghi sembrano essere abbastanza bravi in ​​questo

I tuoi colleghi devono essere davvero eccezionali per non seguire TDD o unit test e non generare mai bug, quindi ad un certo livello dubito che non stiano eseguendo test di unità da soli.

Immagino che i tuoi colleghi stiano eseguendo più test di quanti ne vengano fatti sapere, ma poiché questo fatto non è noto alla direzione, l'organizzazione ne risente di conseguenza perché la direzione ha l'impressione che i test reali non vengano eseguiti e che i numeri di bug siano così bassi il test non è importante e il tempo non verrà pianificato per questo.

Parla con i tuoi colleghi e prova a capire che tipo di test unitari stanno facendo ed emulalo. In un secondo momento è possibile prototipare modi migliori per testare le unità e gli attributi TDD e introdurre lentamente questi concetti al team per facilitare l'adozione.


2
  • Scrivi i tuoi test prima di scrivere il tuo codice.
  • Ogni volta che correggi un bug che non è stato rilevato da un test, scrivi un test per catturare quel bug.

Dovresti essere in grado di ottenere una copertura su ciò che scrivi anche se la tua organizzazione non ha una copertura completa. Come tante cose nella programmazione, l'esperienza di farlo ancora e ancora uno dei modi migliori per essere efficienti.


2

Oltre a tutti gli altri commenti, dal momento che dici che i tuoi colleghi sono bravi a scrivere test di percorsi non felici, perché non chiedere loro di accoppiarsi con te durante la scrittura di alcuni test.

Il modo migliore per imparare è vedere come è fatto e attingere a ciò che impari.


2

Test della scatola nera! Dovresti creare le tue classi / i tuoi metodi pensando ai test. I test devono basarsi sulle specifiche del software e devono essere chiaramente definiti nel diagramma della sequenza (tramite casi d'uso).

Ora, dal momento che potresti non voler fare uno sviluppo guidato dai test ...

Metti la convalida dell'input su tutti i dati in arrivo; non fidarti di nessuno. Il framework .net ha molte eccezioni basate su argomenti non validi, riferimenti null e stati non validi. Dovresti già pensare di utilizzare la convalida dell'input sul livello dell'interfaccia utente, quindi è lo stesso trucco nel middleware.

Ma dovresti davvero fare una sorta di test automatizzato; quella roba salva vite.


2

Nella mia esperienza

Unità di test, se non è completamente automatica, è inutile. È più come un Boss dai capelli a punta potrebbe comprare. Perché ?, perché Test Unit ti ha promesso di risparmiare tempo (e denaro) automatizzando alcuni processi di test. Ma alcuni strumenti dell'unità di test fanno il contrario, costringono i programmatori a lavorare in qualche modo strano e costringono gli altri a creare test troppo estesi. Il più delle volte, non risparmierà l'orario di lavoro ma aumenterà il tempo di passaggio dal QA allo sviluppatore.

UML è un'altra perdita di tempo. una sola lavagna + penna potrebbe fare lo stesso, più economico e veloce.

A proposito, come essere bravi a programmare (ed evitare bug)?

  • a) atomicità. Una funzione che esegue una semplice (o alcune singole attività). Perché è facile da capire, è facile da rintracciare ed è facile risolverlo.

  • b) Omologia. Se, ad esempio, si chiama un database utilizzando una procedura di archiviazione, procedere come per il resto del codice.

  • c) Individuare, ridurre e isolare il "codice creativo". La maggior parte del codice è praticamente copia e incolla. Il codice creativo è l'opposto, un codice nuovo e che funge da prototipo, può fallire. Questo codice è soggetto a bug logici, quindi è importante ridurlo, isolarlo e identificarlo.

  • d) Codice "Thin ice", è il codice che si sa che è "errato" (o potenzialmente pericoloso) ma che è ancora necessario, ad esempio codice non sicuro per un processo multi-task. Evita se puoi.

  • e) Evita il codice black box, questo include il codice che non viene eseguito da te (ad esempio framework) e l'espressione regolare. È facile perdere un bug con questo tipo di codice. Ad esempio, ho lavorato in un progetto usando Jboss e non ho trovato uno ma 10 errori in Jboss (usando l'ultima versione stabile), è stato un PITA trovarli. Evita appositamente l'ibernazione, nasconde l'implementazione, quindi i bug.

  • f) aggiungi commenti nel tuo codice.

  • g) input dell'utente come fonte di bug. identificalo. Ad esempio, SQL Injection è causato da un input dell'utente.

  • h) Identificare l'elemento errato della squadra e separare l'attività assegnata. Alcuni programmatori sono inclini a rovinare il codice.

  • i) Evitare il codice non necessario. Se, ad esempio, la classe necessita di interfaccia, allora usala, altrimenti evita di aggiungere codice irrilevante.

a) e b) sono fondamentali. Ad esempio, ho avuto un problema con un sistema, quando ho fatto clic su un pulsante (salva) non è stato salvato il modulo. Poi ho fatto una lista di controllo:

  • il pulsante funziona? ... sì.
  • il database memorizza qualcosa ?. no, quindi l'errore era in una fase intermedia.
  • quindi, la classe che memorizza nel database funziona ?. no <- ok, ho trovato l'errore. (era collegato con l'autorizzazione del database). Quindi ho controllato non solo questa procedura ma ogni procedura che fa lo stesso (perché l'omologia del codice). Mi ci sono voluti 5 minuti per rintracciare il bug e 1 minuto per risolverlo (e molti altri bug).

E un sidenote

Il più delle volte il QA fa schifo (come una sezione separata di Dev), è inutile se non funzionano nel progetto. Fanno dei test generici e nient'altro. Non sono in grado di identificare la maggior parte dei bug logici. Nel mio caso, stavo lavorando in una banca prestigiosa, un programmatore ha finito un codice e poi lo ha inviato al QA. Il QA ha approvato il codice ed è stato messo in produzione ... quindi il codice non è riuscito (un fallimento epico), sai chi è stato incolpato ?. sì, il programmatore.


2

Un tester e un programmatore affrontano il problema da diverse angolazioni, ma entrambi i ruoli dovrebbero testare completamente la funzionalità e trovare bug. Dove i ruoli differiscono è a fuoco. Un tester classico vede l'applicazione solo dall'esterno (ovvero scatola nera). Sono esperti sui requisiti funzionali dell'app. Un programmatore dovrebbe essere un esperto sia dei requisiti funzionali che del codice (ma tende a concentrarsi maggiormente sul codice).

(Dipende dall'organizzazione se ci si aspetta che i programmatori siano esplicitamente un esperto in materia di requisiti. Indipendentemente da ciò, l'aspettativa implicita è lì - se si progetta qualcosa di sbagliato, tu - non la persona dei requisiti - ricevi la colpa.)

Questo duplice ruolo di esperto sta tassando la mente del programmatore e, fatta eccezione per i più esperti, può ridurre la competenza nei requisiti. Trovo che devo cambiare marcia mentalmente per considerare gli utenti dell'applicazione. Ecco cosa mi aiuta:

  1. Debug ; impostare i punti di interruzione nel codice ed eseguire l'applicazione. Una volta raggiunto un punto di interruzione, attraversa le linee mentre interagisci con l'applicazione.
  2. Test automatizzati ; scrivi il codice che verifica il tuo codice. Questo aiuta solo a livelli sotto l'interfaccia utente.
  3. Conosci i tuoi tester ; potrebbero conoscere l'applicazione meglio di te, quindi impara da loro. Chiedi loro quali sono i punti deboli della tua applicazione e quali tattiche usano per trovare i bug.
  4. Conosci i tuoi utenti ; impara a camminare nei panni dei tuoi utenti. I requisiti funzionali sono l'impronta digitale dei tuoi utenti. Spesso ci sono molte cose che i tuoi utenti conoscono sull'applicazione che potrebbero non risultare chiaramente nei requisiti funzionali. Man mano che comprendi meglio i tuoi utenti - la natura del loro lavoro nel mondo reale e in che modo la tua applicazione dovrebbe aiutarli - capirai meglio quale dovrebbe essere l'applicazione.

2

Penso che tu voglia lavorare su due fronti. Uno è politico, facendo in modo che la tua organizzazione adotti i test ad un certo livello (con la speranza che nel tempo ne adottino di più). Parla con gli ingegneri addetti al controllo qualità al di fuori del tuo posto di lavoro. Trova elenchi di libri sul QA . Dai un'occhiata agli articoli pertinenti di Wikipedia . Acquisire familiarità con i principi e le pratiche del QA. Imparare queste cose ti preparerà a creare il caso più convincente che puoi nella tua organizzazione. Esistono buoni dipartimenti di controllo qualità che aggiungono un valore considerevole alle loro organizzazioni.

Come singolo sviluppatore, adotta strategie da utilizzare nel tuo lavoro. Usa tu stesso TDD sviluppando congiuntamente codice e test. Mantieni i test chiari e ben mantenuti. Se ti viene chiesto perché lo stai facendo, puoi dire che stai impedendo le regressioni e mantiene meglio organizzato il tuo processo di pensiero (entrambi i quali saranno veri). C'è un'arte nello scrivere codice testabile , imparalo. Sii un buon esempio per i tuoi colleghi sviluppatori.

In parte sto predicando a me stesso qui, perché faccio molto meno di queste cose di quanto so che dovrei.

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.