Progettazione di unit test per un sistema con stato


20

sfondo

Test Driven Development è diventato popolare dopo che ho già finito la scuola e nel settore. Sto cercando di impararlo, ma alcune cose importanti mi sfuggono ancora. I sostenitori di TDD dicono molte cose come (di seguito denominato "principio di asserzione singola" o SAP ):

Da qualche tempo sto pensando a come i test TDD possano essere il più semplici, espressivi ed eleganti possibile. Questo articolo esplora un po 'com'è rendere i test il più semplici e decomposti possibile: puntare a una singola affermazione in ogni test.

Fonte: http://www.artima.com/weblogs/viewpost.jsp?thread=35578

Dicono anche cose del genere (di seguito "principio del metodo privato" o PMP ):

Generalmente non testate direttamente i metodi privati. Dal momento che sono privati, considerali un dettaglio di implementazione. Nessuno lo chiamerà mai e si aspetta che funzioni in un modo particolare.

Dovresti invece testare la tua interfaccia pubblica. Se i metodi che chiamano i tuoi metodi privati ​​funzionano come previsto, supponi per estensione che i tuoi metodi privati ​​funzionino correttamente.

Fonte: come si testano i metodi privati?

Situazione

Sto cercando di testare un sistema di elaborazione dei dati con stato. Il sistema può fare cose diverse per lo stesso identico pezzo di dati dato lo stato in cui si trovava prima di ricevere quei dati. Prendi in considerazione un test semplice che costruisca lo stato nel sistema, quindi verifica il comportamento che il metodo dato è destinato a testare.

  • SAP suggerisce che non dovrei testare la "procedura di creazione dello stato", dovrei presumere che lo stato sia quello che mi aspetto dal codice di creazione e quindi testare il cambio di uno stato che sto provando a testare

  • PMP suggerisce che non posso saltare questo passaggio di "costruzione dello stato" e testare i metodi che regolano tale funzionalità in modo indipendente.

Il risultato nel mio codice attuale è stato test gonfiati, complicati, lunghi e difficili da scrivere. E se cambiano le transizioni di stato, i test devono essere cambiati ... il che andrebbe bene con test piccoli ed efficienti ma estremamente dispendiosi in termini di tempo e confusione con questi test a gonfiore prolungato. Come si fa normalmente?


2
Non credo che troverai una soluzione elegante a questo. L'approccio generale non è quello di rendere il sistema con stato, tanto per cominciare, il che non aiuta durante il test di qualcosa che è già stato creato. Riformorarlo come apolide probabilmente non vale neanche il costo.
Doval,


@Doval: Spiegare come rendere qualcosa di simile a un telefono (SIP UserAgent) non-statefull. Il comportamento previsto di questa unità è specificato nell'RFC utilizzando un diagramma di transizione dello stato.
Bart van Ingen Schenau,

Stai copiando / incollando / modificando i tuoi test o stai scrivendo metodi di utilità per condividere impostazioni / smontaggi / funzionalità comuni? Mentre alcuni casi di test possono certamente diventare lunghi e gonfiati, questo non dovrebbe essere così comune. In un sistema con stato, mi aspetterei una routine di installazione comune in cui lo stato finale è un parametro e questa routine ti porta allo stato che desideri testare. Inoltre alla fine di ogni test avrei un metodo di smontaggio che ti riporta allo stato iniziale noto (se necessario) in modo che il tuo metodo di installazione funzionerà correttamente all'inizio del test successivo.
Dunk,

Su una tangente, aggiungerò anche che i diagrammi di stato sono uno strumento di comunicazione e non un decreto di implementazione anche se si trova in una RFC. Finché si incontra la funzionalità descritta, si incontra lo standard. Ho avuto un paio di occasioni in cui ho convertito implementazioni di transizione di stato davvero complicate (come definite nelle RFC) in funzionalità di elaborazione generale davvero semplici. Un caso in cui ricordo di essermi sbarazzato di un paio di migliaia di righe di codice quando mi sono reso conto che, a parte un paio di flag, circa 5 stati hanno fatto esattamente la stessa cosa dopo aver ribattezzato gli elementi comuni "nascosti".
Dunk,

Risposte:


15

Prospettiva:

Quindi facciamo un passo indietro e chiediamo che TDD sta cercando di aiutarci. TDD sta cercando di aiutarci a determinare se il nostro codice è corretto o meno. E per corretto intendo "il codice soddisfa i requisiti aziendali?" Il punto di forza è che sappiamo che saranno necessarie modifiche in futuro e vogliamo assicurarci che il nostro codice rimanga corretto dopo aver apportato tali modifiche.

Ho sollevato questa prospettiva perché penso che sia facile perdersi nei dettagli e perdere di vista ciò che stiamo cercando di ottenere.

Principi - SAP:

Anche se non sono un esperto di TDD, penso che manchi parte di ciò che il principio dell'asserzione singola (SAP) sta cercando di insegnare. SAP può essere riformulato come "testare una cosa alla volta". Ma TOTAT non è facile come SAP.

Testare una cosa alla volta significa che ti concentri su un caso; un percorso; una condizione al contorno; un caso di errore; uno qualunque per test. E l'idea alla base di ciò è che devi sapere cosa si è rotto quando il test case fallisce, in modo da poter risolvere il problema più rapidamente. Se si verificano più condizioni (ad es. Più di una cosa) all'interno di un test e il test fallisce, allora si ha molto più lavoro da fare. Devi prima identificare quale dei casi multipli ha avuto esito negativo e quindi capire perché quel caso non è riuscito.

Se si verifica una cosa alla volta, l'ambito di ricerca è molto più piccolo e il difetto viene identificato più rapidamente. Tieni presente che "testare una cosa alla volta" non ti esclude necessariamente dal guardare più di un output di processo alla volta. Ad esempio, quando collaudo un "percorso noto noto", potrei aspettarmi di vedere un valore risultante specifico fooe un altro valore in bare posso verificarlo foo != barcome parte del mio test. La chiave è raggruppare logicamente i controlli di output in base al caso in esame.

Principi - PMP:

Allo stesso modo, penso che ti manchi un po 'di ciò che il metodo del metodo privato (PMP) deve insegnarci. PMP ci incoraggia a trattare il sistema come una scatola nera. Per un dato input, dovresti ottenere un determinato output. Non ti interessa come la scatola nera genera l'output. Ti interessa solo che le tue uscite siano allineate con i tuoi input.

PMP è davvero una buona prospettiva per esaminare gli aspetti API del tuo codice. Può anche aiutarti a valutare ciò che devi testare. Identifica i tuoi punti di interfaccia e verifica che soddisfino i termini dei loro contratti. Non devi preoccuparti di come i metodi dietro l'interfaccia (aka privati) fanno il loro lavoro. Devi solo verificare che abbiano fatto quello che dovevano fare.


TDD applicato ( per te )

Quindi la tua situazione presenta un po 'di rughe oltre un'applicazione ordinaria. I metodi della tua app sono statiful, quindi il loro output dipende non solo dall'input ma anche da ciò che è stato fatto in precedenza. Sono sicuro che dovrei <insert some lecture>qui che lo stato è orribile e bla bla bla, ma questo non aiuta davvero a risolvere il tuo problema.

Presumo che tu abbia una sorta di tabella del diagramma di stato che mostra i vari stati potenziali e cosa bisogna fare per innescare una transizione. In caso contrario, ne avrai bisogno in quanto aiuterà a esprimere i requisiti aziendali per questo sistema.

I test: in primo luogo, si finirà con una serie di test che attuano il cambiamento di stato. Idealmente, avrai test che esercitano l'intera gamma di cambiamenti di stato che possono verificarsi, ma posso vedere alcuni scenari in cui potresti non aver bisogno di andare fino in fondo.

Successivamente, è necessario creare test per convalidare l'elaborazione dei dati. Alcuni di questi test di stato verranno riutilizzati quando si creano i test di elaborazione dei dati. Ad esempio, supponiamo di avere un metodo Foo()con output diversi basato su an Inite State1stati. Ti consigliamo di utilizzare il ChangeFooToState1test come fase di installazione per testare l'output quando " Foo()è in State1".

Ci sono alcune implicazioni dietro quell'approccio che voglio menzionare. Spoiler, questo è dove farò infuriare i puristi

Prima di tutto, devi accettare che stai usando qualcosa come test in una situazione e una configurazione in un'altra situazione. Da un lato, questa sembra essere una violazione diretta di SAP. Ma se logicamente hai ChangeFooToState1due scopi, allora stai ancora incontrando lo spirito di ciò che SAP ci sta insegnando. Quando è necessario accertarsi che gli Foo()stati delle modifiche vengano utilizzati ChangeFooToState1come test. E quando è necessario convalidare " Foo()l'output in State1", si utilizza ChangeFooToState1come impostazione.

Il secondo elemento è che da un punto di vista pratico, non vorrai test di unità completamente randomizzati per il tuo sistema. È necessario eseguire tutti i test di modifica dello stato prima di eseguire i test di convalida dell'output. SAP è una specie del principio guida dietro quell'ordinamento. Per affermare ciò che dovrebbe essere ovvio, non è possibile utilizzare qualcosa come impostazione se non riesce come test.

Mettendolo insieme:

Usando il diagramma di stato, genererai test per coprire le transizioni. Ancora una volta, usando il diagramma, si generano test per coprire tutti i casi di elaborazione dei dati di input / output guidati dallo stato.

Se segui questo approccio, i bloated, complicated, long, and difficult to writetest dovrebbero essere un po 'più facili da gestire. In generale, dovrebbero finire più piccoli e dovrebbero essere più concisi (cioè meno complicati). Dovresti notare che i test sono anche più disaccoppiati o modulari.

Ora, non sto dicendo che il processo sarà completamente indolore perché scrivere buoni test richiede un certo sforzo. E alcuni saranno ancora difficili perché stai mappando un secondo parametro (stato) su alcuni dei tuoi casi. E a parte questo, dovrebbe essere un po 'più evidente il motivo per cui un sistema senza stato è un metodo più facile da costruire per i test. Ma se adattate questo approccio alla vostra applicazione, dovreste scoprire che siete in grado di dimostrare che l'applicazione funziona correttamente.


11

Solitamente, i dettagli di configurazione vengono estratti in funzioni in modo da non dover ripetere te stesso. In questo modo è necessario modificarlo in un solo punto del test se la funzionalità cambia.

Tuttavia, normalmente non vorrai descrivere nemmeno le tue funzioni di configurazione come gonfie, complicate o lunghe. Questo è un segno che la tua interfaccia necessita di refactoring, perché se è difficile usare i tuoi test, è difficile usare anche il tuo vero codice.

Questo è spesso un segno di mettere troppo in una classe. Se hai requisiti di stato, hai bisogno di una classe che gestisca lo stato e nient'altro. Le classi che lo supportano dovrebbero essere apolidi. Per il tuo esempio SIP, l'analisi di un pacchetto dovrebbe essere completamente senza stato. Puoi avere una classe che analizza un pacchetto, quindi chiama qualcosa come sipStateController.receiveInvite()gestire le transizioni di stato, che a sua volta chiama altre classi senza stato per fare cose come squillare il telefono.

Ciò rende l'impostazione del test unitario per la classe di macchine a stati una semplice questione di alcune chiamate di metodo. Se la tua configurazione per i test delle unità della macchina a stati richiede la creazione di pacchetti, hai inserito troppo in quella classe. Allo stesso modo, la classe del parser dei pacchetti dovrebbe essere relativamente semplice per la creazione del codice di installazione, usando un mock per la classe della macchina a stati.

In altre parole, non puoi evitare del tutto lo stato, ma puoi minimizzarlo e isolarlo.


Solo per la cronaca, l'esempio SIP era mio, non dall'OP. E alcune macchine a stati potrebbero richiedere più di alcune chiamate di metodo per riportarle nello stato giusto per un certo test.
Bart van Ingen Schenau,

+1 per "non puoi evitare del tutto lo stato, ma puoi minimizzarlo e isolarlo". Non potrei essere d'accordo. Lo stato è un male necessario nel software.
Brandon,

0

L'idea principale di TDD è che, scrivendo prima i test, si finisce con un sistema che è almeno facile da testare. Speriamo che funzioni, sia mantenibile, ben documentato e così via, ma in caso contrario, almeno è comunque facile da testare.

Quindi, se fai TDD e finisci con un sistema che è difficile da testare, qualcosa è andato storto. Forse alcune cose private dovrebbero essere pubbliche, perché ne hai bisogno per essere testate. Forse non stai lavorando al giusto livello di astrazione; qualcosa di semplice come un elenco è a un livello, ma un valore a un altro. O forse stai dando troppo peso ai consigli non applicabili nel tuo contesto, o il tuo problema è solo difficile. O, naturalmente, forse il tuo design è semplicemente cattivo.

Qualunque sia la causa, probabilmente non tornerai indietro e riscriverai il tuo sistema per renderlo più testabile con un semplice codice di prova. Quindi probabilmente il piano migliore è usare alcune tecniche di test leggermente più elaborate, come:

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.