Questo è un estratto dall'eBook Testing Unit Affinity, di Marc Clifton, gentilmente fornito da Syncfusion.
Gli articoli precedenti hanno toccato una serie di preoccupazioni e benefici dei test unitari. Questo articolo è un aspetto più formale del costo e dei benefici dei test unitari.
Il codice di test dell'unità è un'entità separata dal codice in fase di test, tuttavia condivide molti degli stessi problemi richiesti dal codice di produzione:
Inoltre, i test unitari possono anche:
Nel determinare se i test possono essere scritti rispetto a un singolo metodo, è necessario considerare:
Uno dovrebbe rendersi conto che il conteggio delle righe del codice per verificare anche un metodo semplice potrebbe essere considerevolmente più grande del conteggio delle righe del metodo stesso.
La modifica del codice di produzione può spesso invalidare i test unitari. Le modifiche al codice si suddividono approssimativamente in due categorie:
Il primo di solito ha pochi o nessun requisiti di manutenzione sui test unitari esistenti. Quest'ultima, tuttavia, richiede spesso una considerevole rielaborazione dei test unitari, a seconda della complessità della modifica:
Come accennato in precedenza, i test unitari, in particolare in un processo basato sui test, impongono determinati paradigmi di architettura e implementazione minimi. Per supportare ulteriormente la facilità di impostazione o eliminazione di alcune aree del codice, il test dell'unità può anche beneficiare di considerazioni di architettura più complesse, come l'inversione del controllo.
Come minimo, la maggior parte delle classi dovrebbe facilitare il beffeggiamento di qualsiasi oggetto. Ciò può migliorare significativamente le prestazioni dei test, ad esempio testare un metodo che esegue controlli dell'integrità delle chiavi esterne (anziché affidarsi al database per riportare gli errori in un secondo momento) non dovrebbe richiedere un'impostazione complessa o la distruzione dello scenario di test nel database si. Inoltre, non dovrebbe richiedere il metodo per interrogare effettivamente il database. Questi sono tutti i riscontri relativi alle prestazioni del test e aggiungono dipendenze su una connessione autenticata dal vivo con il database e, pertanto, potrebbero non gestire un'altra workstation che esegue esattamente lo stesso test nello stesso momento. Invece, prendendo in giro la connessione al database, il test dell'unità può facilmente impostare lo scenario in memoria e passare l'oggetto di connessione come interfaccia.
Tuttavia, semplicemente deridere una classe non è necessariamente la migliore pratica, potrebbe essere meglio rifattorizzare il codice in modo che tutte le informazioni necessarie al metodo siano ottenute separatamente, separando l'acquisizione dei dati dal calcolo dei dati. Ora, il calcolo può essere eseguito senza prendere in giro l'oggetto responsabile dell'acquisizione dei dati, che semplifica ulteriormente la configurazione del test.
Ci sono un paio di strategie di mitigazione dei costi che dovrebbero essere considerate.
Il modo più efficace per ridurre il costo dei test unitari è evitare di dover scrivere il test. Mentre questo sembra ovvio, come viene raggiunto? La risposta è garantire che i dati passati al metodo siano corretti, in altre parole, input corretto, output corretto (il contrario di "garbage in, garbage out"). Sì, probabilmente vuoi ancora testare il calcolo stesso, ma se puoi garantire che il chiamante soddisfi il contratto, non è particolarmente necessario testare il metodo per vedere se gestisce parametri errati (violazioni del contratto).
Questa è una pendenza un po 'scivolosa perché non hai idea di come il metodo potrebbe essere chiamato in futuro, infatti, potresti volere che il metodo continui a convalidare il suo contratto, ma nel contesto in cui è attualmente utilizzato, se è possibile garantire che il contratto sia sempre rispettato, allora non vi è alcun punto di forza nella scrittura di test contro il contratto.
Come assicurate gli input corretti? Per i valori che provengono da un'interfaccia utente, filtrare e controllare adeguatamente l'interazione dell'utente per pre-filtrare i valori è un approccio. Un approccio più sofisticato consiste nel definire tipi specializzati piuttosto che affidarsi a tipi di scopi generali. Considera il metodo Divide descritto in precedenza:
public static int Divide (numeratore int, denominatore int) if (denominatore == 0) lanciare una nuova ArgumentOutOfRangeException ("Denominatore non può essere 0."); numeratore / denominatore di ritorno;
Se il denominatore era un tipo specializzato che garantiva un valore diverso da zero:
public class NonZeroDouble protected int val; int pubblico Valore get return val; set if (value == 0) throw new ArgumentOutOfRangeException ("Il valore non può essere 0."); val = valore;
il metodo Divide non avrebbe mai avuto bisogno di testare per questo caso:
////// Un esempio di utilizzo della specificità del tipo per evitare un test contrattuale. /// public static int Divide (numeratore int, nonZeroDouble denominatore) return numerator / denominator.Value;
Quando si considera che ciò migliora la specificità del tipo dell'applicazione e stabilisce (si spera) tipi riutilizzabili, ci si rende conto di come questo eviti di dover scrivere un gran numero di test unitari perché il codice spesso utilizza tipi troppo generali.
Chiediti: il mio metodo dovrebbe essere responsabile della gestione di eccezioni da terze parti, come servizi Web, database, connessioni di rete, ecc.? Si può affermare che la risposta è "no". Certo, ciò richiede un ulteriore lavoro in anticipo: l'API di terze parti (o anche framework) ha bisogno di un wrapper che gestisca l'eccezione e un'architettura in cui lo stato interno del l'applicazione può essere ripristinata quando si verifica un'eccezione e probabilmente dovrebbe essere implementata. Tuttavia, questi sono probabilmente dei miglioramenti utili all'applicazione.
Gli esempi precedenti - input corretti, sistemi di tipi specializzati, evitando eccezioni di terze parti - spingono il problema a scopi più generali e possibilmente riutilizzabili. Ciò aiuta ad evitare di scrivere la stessa validazione del contratto o simili, i test unitari di gestione delle eccezioni e consente di concentrarsi invece sui test che convalidano ciò che il metodo dovrebbe fare in condizioni normali, cioè il calcolo stesso.
Come accennato in precedenza, ci sono dei sicuri vantaggi economici per il test unitario.
Uno degli ovvi vantaggi è il processo di formalizzazione dei requisiti del codice interno da requisiti di usabilità / processo esterni. Quando si passa attraverso questo esercizio, la direzione con l'architettura generale è in genere un beneficio collaterale. Più concretamente, sviluppando una serie di test che soddisfa un requisito specifico unità prospettiva (piuttosto che il test di integrazione prospettiva) è la prova oggettiva che il codice attua il requisito.
Il test di regressione è un altro beneficio (spesso misurabile). Man mano che la base di codice cresce, verificando che il codice esistente funzioni ancora come previsto, consente di risparmiare considerevoli tempi di test manuali ed evita lo scenario "oops, non abbiamo testato per quello". Inoltre, quando viene segnalato un errore, può essere corretto immediatamente, spesso salvando gli altri membri della squadra il notevole mal di testa di chiedersi perché qualcosa su cui stavano facendo affidamento improvvisamente non funzioni correttamente.
I test unitari verificano non solo che un metodo si gestisce correttamente quando vengono forniti input errati o eccezioni di terze parti (come descritto in precedenza, provano a ridurre questo tipo di test), ma anche come si prevede che il metodo si comporti in condizioni normali. Ciò fornisce una preziosa documentazione agli sviluppatori, in particolare ai nuovi membri del team: tramite il test delle unità possono facilmente comprendere i requisiti di configurazione e i casi d'uso. Se il tuo progetto viene sottoposto a un significativo refactoring architettonico, i nuovi test unitari possono essere utilizzati per guidare gli sviluppatori nella rielaborazione del codice dipendente.
Come descritto in precedenza, un'architettura più robusta attraverso l'uso di interfacce, inversione di controllo, tipi specializzati, ecc., Che facilitano tutti i test unitari-anche migliorare la robustezza dell'applicazione. I requisiti cambiano, anche durante lo sviluppo, e un'architettura ben congegnata è in grado di gestire tali cambiamenti in modo considerevolmente migliore rispetto a un'applicazione che ha poca o poca considerazione architettonica.
Piuttosto che affidare a un programmatore junior un requisito di alto livello da implementare a livello di competenze del programmatore, puoi invece garantire un livello superiore di codice e successo (e fornire un'esperienza di insegnamento) facendo in modo che il programmatore junior codifichi l'implementazione contro il test piuttosto che il requisito. Questo elimina molte cattive pratiche o congetture che un programmatore junior finisce per implementare (siamo stati tutti lì) e riduce la rielaborazione che uno sviluppatore più anziano deve fare in futuro.
Esistono diversi tipi di recensioni del codice. I test unitari possono ridurre la quantità di tempo speso per la revisione del codice per problemi di architettura perché tendono ad applicare l'architettura. Inoltre, i test unitari convalidano il calcolo e possono anche essere utilizzati per convalidare tutti i percorsi del codice per un determinato metodo. Ciò rende le revisioni del codice quasi inutili: il test dell'unità diventa un'autovalutazione del codice.
Un interessante effetto collaterale della conversione dell'usabilità esterna o dei requisiti di processo in test di codice formalizzati (e la loro architettura di supporto) è che:
Queste scoperte, come risultato del processo di test unitario, identificano i problemi in precedenza nel processo di sviluppo, che di solito aiuta a ridurre la confusione, la rielaborazione e, quindi, riduce i costi.