Test unitario succintamente strategie per i test unitari

Gli approcci di test dipendono da dove ti trovi nel progetto e dal tuo "budget", in termini di tempo, denaro, risorse umane, necessità, ecc. Idealmente, i test unitari sono preventivati ​​nel processo di sviluppo, ma realisticamente, spesso incontriamo programmi esistenti o legacy che hanno poca o nessuna copertura di codice ma devono essere aggiornati o mantenuti. 

Lo scenario peggiore è un prodotto che è attualmente in fase di sviluppo ma mostra un numero maggiore di errori durante il suo sviluppo, sempre con una copertura di codice minima o nulla. In qualità di product manager, sia all'inizio di uno sforzo di sviluppo che a seguito della consegna di un'applicazione esistente, è importante sviluppare una ragionevole strategia di test unitario. 

Ricorda che i test unitari dovrebbero fornire benefici misurabili al tuo progetto per compensare la responsabilità del loro sviluppo, della manutenzione e dei propri test. Inoltre, la strategia adottata per il test dell'unità può influire sull'architettura dell'applicazione. Anche se questa è quasi sempre una buona cosa, potrebbe introdurre un sovraccarico non necessario per le tue esigenze.

A partire dai requisiti

Se stai iniziando un'applicazione sufficientemente complessa da una lavagna pulita e tutto ciò che è nelle tue mani è un insieme di requisiti, considera la seguente guida.

Priorità ai requisiti computazionali

Dare priorità ai requisiti computazionali dell'applicazione per determinare dove si trova la complessità. La complessità può essere determinata scoprendo il numero di stati che un particolare calcolo deve soddisfare, oppure può essere il risultato di un ampio set di dati di input richiesti per eseguire il calcolo, oppure potrebbe essere semplicemente algoritmicamente complesso, come ad esempio fare analisi del caso di errore sull'anello di ridondanza di un satellite. Considerare anche dove il codice è suscettibile di cambiamenti in futuro come il risultato di requisiti di modifica sconosciuti. Mentre sembra che sia necessaria la chiaroveggenza, un abile architetto di software può classificare il codice in generale (risolvendo un problema comune) e specifico del dominio (risolvendo un problema di requisiti specifici). Quest'ultimo diventa un candidato per il cambiamento futuro.

Mentre scrivere test di unità per funzioni banali è facile, veloce e gratificante nel numero di casi di test che il programma sforna, sono i test meno costosi: richiedono tempo per scrivere e, perché molto probabilmente verranno scritti correttamente per cominciare e molto probabilmente non cambieranno nel tempo, sono i meno utili con l'aumentare della base di codice dell'applicazione. Invece, focalizza la tua strategia di test dell'unità sul codice specifico e complesso del dominio.

Seleziona un'architettura

Uno dei vantaggi dell'inizio di un progetto da una serie di requisiti è la possibilità di creare l'architettura (o selezionare un'architettura di terze parti) come parte del processo di sviluppo. I framework di terze parti che consentono di sfruttare architetture quali l'inversione del controllo (e il relativo concetto di dipendenza dall'iniezione), nonché le architetture formali come Model-View-Controller (MVC) e Model-View-ViewModel (MVVM) facilitano test delle unità per il semplice motivo che un'architettura modulare è in genere più semplice da testare. Queste architetture si separano:

  • La presentazione (vista).
  • Il modello (responsabile per la persistenza e la rappresentazione dei dati).
  • Il controller (dove dovrebbero esserci i calcoli).

Mentre alcuni aspetti del modello potrebbero essere candidati per il test unitario, la maggior parte dei test unitari saranno probabilmente scritti contro i metodi nel controller o nel modello di vista, che è dove vengono implementati i calcoli sul modello o sulla vista.

Fase di manutenzione

I test unitari possono essere di beneficio anche se si è coinvolti nella manutenzione di un'applicazione, uno che richiede l'aggiunta di nuove funzionalità a un'applicazione esistente o semplicemente la risoluzione di bug di un'applicazione legacy. Esistono diversi approcci che è possibile adottare per un'applicazione esistente e domande alla base di tali approcci che possono determinare il rapporto costo-efficacia dei test unitari:

  • Scrivi test unitari solo per nuove funzionalità e correzioni di bug? La funzionalità o il bug risolvono qualcosa che trarrà beneficio dai test di regressione, oppure è un problema isolato e una tantum che è più facile testare durante i test di integrazione?
  • Inizi a scrivere test unitari contro funzionalità esistenti? In tal caso, come stabilire le priorità per le funzionalità da testare per prime?
  • La base di codice esistente funziona bene con i test di unità o il codice richiede prima il refactoring per isolare le unità di codice?
  • Quali configurazioni o eliminazioni sono necessarie per la funzionalità o la verifica degli errori?
  • Quali dipendenze possono essere scoperte sulle modifiche al codice che possono causare effetti collaterali in altri codici e se i test unitari vengono ampliati per testare il comportamento del codice dipendente?

Passare alla fase di manutenzione di un'applicazione legacy che manca di test delle unità non è banale: la pianificazione, la considerazione e l'analisi del codice possono spesso richiedere più risorse rispetto alla semplice correzione del bug. Tuttavia, l'uso giudizioso dei test unitari può essere conveniente, e anche se questo non è sempre facile da determinare, vale la pena farlo, se non altro per avere una comprensione più approfondita del codice base.


Determina il tuo processo

Ci sono tre strategie che si possono intraprendere per quanto riguarda il processo di unit test: "Test-Driven Development," Code First, "e, sebbene possa sembrare antitetico al tema di questo libro, il processo" No Unit Test ".

Sviluppo guidato dai test

Un campo è "Test-Driven Development", riepilogato dal seguente flusso di lavoro:

Dato un requisito di calcolo (vedere la sezione precedente), in primo luogo, scrivere uno stub per il metodo.

  • Se sono necessarie dipendenze da altri oggetti non ancora implementati (oggetti passati come parametri al metodo o restituiti dal metodo), implementarli come interfacce vuote.
  • Se mancano delle proprietà, implementare gli stub per le proprietà necessarie per verificare i risultati.
  • Scrivi eventuali requisiti di test di installazione o di smontaggio.
  • Scrivi i test. Le ragioni per scrivere qualsiasi stub prima scrivere il test sono: in primo luogo, per trarre vantaggio da IntelliSense durante la scrittura del test; in secondo luogo, per stabilire che il codice compila ancora; e terzo, per garantire che il metodo da testare, i suoi parametri, interfacce e proprietà siano tutti sincronizzati per quanto riguarda la denominazione.
  • Esegui i test, verificando che falliscano.
  • Codifica l'implementazione.
  • Esegui i test, verificando che abbiano successo.

In pratica, è più difficile di quanto sembri. È facile cadere preda di test di scrittura che non sono economicamente convenienti, e spesso si scopre che il metodo testato non è un'unità sufficientemente dettagliata per essere effettivamente un buon candidato per un test. Forse il metodo sta facendo troppo, richiede troppa installazione o rimozione, o ha dipendenze da troppi altri oggetti che devono essere tutti inizializzati in uno stato noto. Queste sono tutte cose che vengono scoperte più facilmente quando si scrive il codice, non il test.

Uno dei vantaggi di un approccio basato sui test è che il processo instilla la disciplina del test unitario e scrive per prima cosa i test unitari. È facile determinare se lo sviluppatore sta seguendo il processo. Con la pratica, si può diventare facili anche rendendo il processo economico.

Un altro vantaggio di un approccio basato sui test è che, per sua natura, applica un tipo di architettura. Sarebbe assurdo, ma è possibile scrivere un test unitario che inizializza un modulo, inserisce valori in un controllo e quindi chiama un metodo che dovrebbe eseguire alcuni calcoli sui valori, come richiesto da questo codice (effettivamente trovato qui):

private void btnCalculate_Click (mittente dell'oggetto, System.EventArgs e) double Principal, AnnualRate, InterestEarned; double FutureValue, RatePerPeriod; int NumberOfPeriods, CompoundType; Principal = Double.Parse (txtPrincipal.Text); AnnualRate = Double.Parse (txtInterest.Text) / 100; if (rdoMonthly.Checked) CompoundType = 12; else if (rdoQuarterly.Checked) CompoundType = 4; else if (rdoSemiannually.Checked) CompoundType = 2; else CompoundType = 1; NumberOfPeriods = Int32.Parse (txtPeriods.Text); double i = AnnualRate / CompoundType; int n = CompoundType * NumberOfPeriods; RatePerPeriod = AnnualRate / NumberOfPeriods; FutureValue = Principal * Math.Pow (1 + i, n); InterestEarned = FutureValue - Principal; txtInterestEarned.Text = InterestEarned.ToString ("C"); txtAmountEarned.Text = FutureValue.ToString ("C"); 

Il codice precedente non è testabile in quanto è impigliato con il gestore di eventi e l'interfaccia utente. Piuttosto, si potrebbe scrivere il metodo di calcolo dell'interesse composto:

public enum CompoundType Annually = 1, SemiAnnually = 2, Quarterly = 4, Monthly = 12 private double CompoundInterestCalculation (double principal, double annualRate, CompoundType compoundType, int periods) double annualRateDecimal = annualRate / 100.0; double i = annualRateDecimal / (int) compoundType; int n = (int) compoundType * periodi; double ratePerPeriod = annualRateDecimal / periods; double futureValue = principal * Math.Pow (1 + i, n); double interestEaned = futureValue - principal; return interestEaned; 

che consentirebbe quindi di scrivere un semplice test:

[TestMethod] public void CompoundInterestTest () double interest = CompoundInterestCalculation (2500, 7.55, CompoundType.Monthly, 4); Assert.AreEqual (878.21, interesse, 0,01); 

Inoltre, utilizzando test parametrizzati, sarebbe semplice testare ogni tipo di composto, un intervallo di anni e interessi e importi principali diversi.

L'approccio basato sui test in realtà facilita un processo di sviluppo più formalizzato mediante la scoperta di unità testabili reali e l'isolamento dalle dipendenze che attraversano i confini.

Codice prima, prova secondo

La prima codifica è più naturale se non altro perché è il modo abituale in cui vengono sviluppate le applicazioni. Il requisito e la sua implementazione possono anche sembrare abbastanza facili a prima vista, così che scrivere diversi test unitari sembra un cattivo uso del tempo. Altri fattori come le scadenze possono forzare un progetto in un "solo ottenere il codice scritto in modo che possiamo spedire" il processo di sviluppo.

Il problema con l'approccio code-first è che è facile scrivere codice che richiede il tipo di test che abbiamo visto prima. Il codice richiede innanzitutto una disciplina attiva per testare il codice che è stato scritto. Questa disciplina è incredibilmente difficile da raggiungere, soprattutto perché c'è sempre la nuova funzionalità da implementare.

Richiede anche l'intelligenza, se vuoi, per evitare di scrivere codice impigliato, di attraversamento dei confini e la disciplina per farlo. Chi non ha fatto clic su un pulsante nel designer di Visual Studio e ha codificato il calcolo dell'evento proprio lì nello stub creato da Visual Studio per te? È semplice e poiché lo strumento ti sta indirizzando in quella direzione, l'ingenuo programmatore penserà che questo sia il modo giusto di codificare.

Questo approccio richiede un'attenta considerazione delle capacità e della disciplina della squadra e richiede un monitoraggio più attento della squadra, specialmente durante periodi di stress elevato in cui gli approcci disciplinati tendono ad abbattere. Certo, una disciplina basata sui test può anche essere buttata fuori mentre le scadenze incombono, ma quella tende a essere una decisione consapevole di fare un'eccezione, mentre può facilmente diventare la regola in un primo approccio al codice.

Nessun test unitario

Solo perché non hai test unitari non significa che stai buttando fuori i test. Può essere semplicemente che il test enfatizzi le procedure di test di accettazione o test di integrazione.

Bilanciamento delle strategie di test

Un processo di test unitario economicamente efficace richiede un equilibrio tra le strategie Test-Driven Development, Code First, Test Second e "Test Some Other Way". Dovrebbe essere sempre preso in considerazione il rapporto costo-efficacia dei test unitari, oltre a fattori come l'esperienza degli sviluppatori nel team. Come manager, potresti non voler sentire che un approccio basato sui test è una buona idea se la tua squadra è abbastanza verde e hai bisogno del processo per instillare disciplina e approccio.