Test unitario brevemente guarda prima di saltare

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.

Codice test unitario e codice testati

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:

  • Pianificazione
  • Sviluppo
  • Test (sì, i test unitari devono essere testati)

Inoltre, i test unitari possono anche:

  • Avere una base di codice più ampia rispetto al codice di produzione.
  • È necessario essere sincronizzati quando il codice di produzione cambia.
  • Tendono a far rispettare direzioni architettoniche e modelli di implementazione.

La base del codice di prova dell'unità può essere più grande del codice di produzione

Nel determinare se i test possono essere scritti rispetto a un singolo metodo, è necessario considerare:

  • Convalida il contratto?
  • Il calcolo funziona correttamente?
  • Lo stato interno dell'oggetto è impostato correttamente?
  • Restituisce l'oggetto ad uno stato "sano" se si verifica un'eccezione?
  • Sono stati testati tutti i percorsi di codice?
  • Quali sono i requisiti di setup o di smontaggio del metodo?

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.


Manutenzione dei test delle unità

La modifica del codice di produzione può spesso invalidare i test unitari. Le modifiche al codice si suddividono approssimativamente in due categorie:

  • Nuovo codice o modifiche al codice esistente che migliorano l'esperienza dell'utente.
  • Riorganizzazione significativa per supportare i requisiti che l'architettura esistente non supporta.

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:

  • Rifattorizzazione di parametri di classe concreti su interfacce o classi astratte.
  • Rifattorizzazione della gerarchia di classi.
  • Sostituzione di una tecnologia di terze parti con un'altra.
  • Refactoring del codice per attività asincrone o di supporto.
  • Altri:
    • Esempio: passaggio da una classe di database concreta come SqlConnection a IDbConnection, in modo che il codice supporti diversi database e richieda la rielaborazione dei test unitari che richiamano metodi che dipendono da classi concrete per i loro parametri.
    • Esempio: modifica di un modello per utilizzare un formato di serializzazione standard, ad esempio XML, anziché una metodologia di serializzazione personalizzata.
    • Esempio: il passaggio da un ORM interno a un ORM di terze parti come Entity Framework può richiedere modifiche considerevoli all'installazione o alle analisi dei test delle unità.

Il test unitario impone un paradigma di architettura?

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.


Prestazioni del test unitario

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.


Costi attenuanti

Ci sono un paio di strategie di mitigazione dei costi che dovrebbero essere considerate.

Ingressi corretti

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.

Evitare eccezioni di terze parti

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.

Evitare di scrivere gli stessi test per ciascun metodo

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.


Vantaggi economici

Come accennato in precedenza, ci sono dei sicuri vantaggi economici per il test unitario.

Coding al requisito

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.

Riduce gli errori a valle

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 casi di test forniscono una forma di documentazione

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.

Applicare un paradigma di architettura migliora l'architettura

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.

Programmatori Junior

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.

Codice Recensioni

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.

Conversione dei requisiti in test

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:

  • I problemi con i requisiti sono spesso scoperti.
  • I requisiti architetturali vengono messi alla luce.
  • Presupposti e altre lacune nei requisiti sono identificati.

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.