Dovrebbe essere "Arrange-Assert-Act-Assert"?


94

Per quanto riguarda il classico modello di prova di Arrange-Act-Assert , mi trovo spesso ad aggiungere una controasserzione che precede Act. In questo modo so che l'affermazione passeggera è davvero passiva come risultato dell'azione.

Lo considero analogo al red in red-green-refactor, dove solo se ho visto la barra rossa nel corso dei miei test so che la barra verde significa che ho scritto un codice che fa la differenza. Se scrivo un test di superamento, qualsiasi codice lo soddisferà; allo stesso modo, per quanto riguarda Arrange-Assert-Act-Assert, se la mia prima asserzione fallisce, so che qualsiasi atto avrebbe superato l'asserzione finale, quindi non stava effettivamente verificando nulla sull'Atto.

I tuoi test seguono questo schema? Perché o perché no?

Chiarimento sull'aggiornamento : l'asserzione iniziale è essenzialmente l'opposto dell'asserzione finale. Non è un'affermazione che Arrange abbia funzionato; è un'affermazione che Act non ha ancora funzionato.

Risposte:


121

Questa non è la cosa più comune da fare, ma comunque abbastanza comune da avere un proprio nome. Questa tecnica è chiamata Asserzione di guardia . È possibile trovare una descrizione dettagliata a pagina 490 nell'eccellente libro xUnit Test Patterns di Gerard Meszaros (altamente raccomandato).

Normalmente, non utilizzo questo schema da solo, poiché trovo più corretto scrivere un test specifico che convalidi qualunque precondizione sento il bisogno di garantire. Un test di questo tipo dovrebbe sempre fallire se la precondizione fallisce, e questo significa che non ne ho bisogno incorporato in tutti gli altri test. Ciò fornisce un migliore isolamento delle preoccupazioni, poiché un caso di test verifica solo una cosa.

Potrebbero esserci molte condizioni preliminari che devono essere soddisfatte per un determinato caso di test, quindi potrebbe essere necessaria più di un'asserzione di guardia. Invece di ripeterli in tutti i test, avere un (e solo uno) test per ogni precondizione mantiene il codice del test più gestibile, poiché in questo modo si avranno meno ripetizioni.


+1, risposta molto buona. L'ultima parte è particolarmente importante, perché mostra che puoi proteggere le cose come un test unitario separato.
murrekatt,

3
In genere l'ho fatto anche in questo modo, ma c'è un problema con un test separato per garantire le precondizioni (specialmente con una base di codice grande con requisiti mutevoli): il test delle precondizioni verrà modificato nel tempo e non sarà più sincronizzato con il "principale" test che presuppone tali precondizioni. Quindi le condizioni preliminari possono essere tutte buone e verdi, ma quelle condizioni non sono soddisfatte nel test principale, che ora mostra sempre verde e bene. Ma se le condizioni preliminari fossero state nel test principale, avrebbero fallito. Ti sei imbattuto in questo problema e hai trovato una bella soluzione?
nchaud

2
Se modifichi molto i tuoi test, potresti avere altri problemi , perché ciò tenderà a rendere i tuoi test meno affidabili. Anche di fronte al cambiamento dei requisiti, prendere in considerazione la progettazione del codice in modo solo append .
Mark Seemann

@MarkSeemann Hai ragione, dobbiamo ridurre al minimo la ripetizione, ma dall'altra parte ci possono essere molte cose che possono influire su Arrange per il test specifico, anche se il test per Arrange stesso passerebbe. Ad esempio, la pulizia per il test Arrange o dopo un altro test è stata difettosa e Arrange non sarebbe lo stesso del test Arrange.
Rekshino


8

Ecco un esempio.

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
    range.encompass(7);
    assertTrue(range.includes(7));
}

Potrebbe essere che ho scritto Range.includes()semplicemente per restituire vero. Non l'ho fatto, ma posso immaginare che avrei potuto. Oppure avrei potuto scrivere in modo sbagliato in molti altri modi. Speravo e mi aspettavo che con TDD avessi effettivamente capito bene - includes()funziona e basta - ma forse non l'ho fatto. Quindi la prima asserzione è un controllo di sanità mentale, per garantire che la seconda asserzione sia davvero significativa.

Letto da solo, assertTrue(range.includes(7));sta dicendo: "asserisci che l'intervallo modificato include 7". Leggi nel contesto della prima asserzione, sta dicendo: "asserisci che invocare encompass () fa sì che includa 7. E poiché encompass è l'unità che stiamo testando, penso che abbia un valore (piccolo).

Accetto la mia risposta; molti altri hanno frainteso la mia domanda riguardante il test del setup. Penso che questo sia leggermente diverso.


Grazie per essere tornato con un esempio, Carl. Bene, nella parte rossa del ciclo TDD, fino a quando encompass () fa davvero qualcosa; la prima affermazione è inutile, è solo una duplicazione della seconda. Al verde, inizia a essere utile. Ha senso durante il refactoring. Potrebbe essere bello avere un framework UT che lo faccia automaticamente.
philant

Supponiamo che tu TDD quella classe Range, non ci sarà un altro test fallito che prova il Range ctor, quando lo romperai?
philant

1
@philippe: non sono sicuro di aver capito la domanda. Il costruttore Range e include () hanno i propri unit test. Potresti approfondire, per favore?
Carl Manaster

Affinché la prima asserzione assertFalse (range.includes (7)) fallisca, è necessario avere un difetto nel Range Constructor. Quindi volevo chiedere se i test per il costruttore Range non si interrompono contemporaneamente a quella affermazione. E che dire dell'asserzione dopo l'Act su un altro valore: ad esempio assertFalse (range.includes (6))?
philant

1
La costruzione dell'intervallo, a mio avviso, viene prima di funzioni come include (). Quindi, anche se sono d'accordo, solo un costruttore difettoso (o un include () difettoso) farebbe fallire quella prima asserzione, il test del costruttore non includerebbe una chiamata a includes (). Sì, tutte le funzioni fino alla prima asserzione sono già testate. Ma questa iniziale affermazione negativa sta comunicando qualcosa e, a mio avviso, qualcosa di utile. Anche se ogni affermazione di questo tipo passa quando viene scritta inizialmente.
Carl Manaster,

7

Un Arrange-Assert-Act-Asserttest può sempre essere modificato in due test:

1. Arrange-Assert

e

2. Arrange-Act-Assert

Il primo test farà valere solo ciò che è stato impostato nella fase Arrange, e il secondo test farà valere solo ciò che è accaduto nella fase Act.

Questo ha il vantaggio di fornire un feedback più preciso sul fatto che sia fallita la fase Arrange o Act, mentre nell'originale Arrange-Assert-Act-Assertquesti sono fusi e dovresti scavare più a fondo ed esaminare esattamente quale asserzione ha fallito e perché ha fallito per sapere se è stato l'Arrange or Act a fallire.

Soddisfa anche meglio l'intenzione di test di unità, poiché stai separando il tuo test in unità indipendenti più piccole.

Infine, tieni presente che ogni volta che vedi sezioni Arrange simili in test diversi, dovresti provare a estrarle in metodi di supporto condivisi, in modo che i tuoi test siano più DRY e più manutenibili in futuro.


3

Ora lo sto facendo. AAAA di tipo diverso

Arrange - setup
Act - what is being tested
Assemble - what is optionally needed to perform the assert
Assert - the actual assertions

Esempio di un test di aggiornamento:

Arrange: 
    New object as NewObject
    Set properties of NewObject
    Save the NewObject
    Read the object as ReadObject

Act: 
    Change the ReadObject
    Save the ReadObject

Assemble: 
    Read the object as ReadUpdated

Assert: 
    Compare ReadUpdated with ReadObject properties

Il motivo è che l'ACT non contiene la lettura del ReadUpdated è perché non fa parte dell'atto. L'atto è solo cambiare e salvare. Quindi, davvero, ARRANGE ReadUpdated for assertion, chiamo ASSEMBLE per assertion. Questo per evitare di confondere la sezione ARRANGE

ASSERT dovrebbe contenere solo asserzioni. Ciò lascia ASSEMBLE tra ACT e ASSERT che imposta l'assert.

Infine, se stai fallendo nell'Arrange, i tuoi test non sono corretti perché dovresti avere altri test per prevenire / trovare questi bug banali . Perché per lo scenario che presento, dovrebbero già esserci altri test che testano READ e CREATE. Se crei una "asserzione di guardia", potresti rompere DRY e creare manutenzione.


1

Lanciare un'asserzione di "controllo di integrità" per verificare lo stato prima di eseguire l'azione che si sta testando è una vecchia tecnica. Di solito li scrivo come scaffolding di test per dimostrare a me stesso che il test fa ciò che mi aspetto e li rimuovo in seguito per evitare di ingombrare i test con gli scaffolding di test. A volte, lasciare le impalcature aiuta il test a servire da narrazione.


1

Ho già letto di questa tecnica - forse da te tra l'altro - ma non la uso; soprattutto perché sono abituato al modulo tripla A per i miei test unitari.

Ora, mi sto incuriosendo e ho alcune domande: come scrivi il tuo test, fai fallire questa asserzione, seguendo un ciclo di refactoring rosso-verde-rosso-verde, o lo aggiungi in seguito?

A volte fallisci, forse dopo aver rifattorizzato il codice? Cosa ti dice questo? Forse potresti condividere un esempio in cui ha aiutato. Grazie.


In genere non costringo l'asserzione iniziale a fallire - dopotutto, non dovrebbe fallire, come dovrebbe fare un'asserzione TDD, prima che il suo metodo sia scritto. Io non scrivo, quando scrivo, prima , proprio nel normale corso della scrittura del test, non dopo. Onestamente, non ricordo che sia fallito, forse questo suggerisce che è una perdita di tempo. Cercherò di trovare un esempio, ma al momento non ne ho uno in mente. Grazie per le domande; sono utili.
Carl Manaster,

1

L'ho già fatto durante le indagini su un test fallito.

Dopo un considerevole graffio alla testa, ho stabilito che la causa era che i metodi chiamati durante "Arrange" non funzionavano correttamente. Il fallimento del test è stato fuorviante. Ho aggiunto un'asserzione dopo la disposizione. Ciò ha fatto fallire il test in un punto che ha evidenziato il problema reale.

Penso che qui ci sia anche un odore di codice se la parte Arrange del test è troppo lunga e complicata.


Un punto minore: considererei Arrange troppo complicato più un odore di design che un odore di codice - a volte il design è tale che solo un Arrange complicato ti permetterà di testare l'unità. Lo menziono perché quella situazione richiede una soluzione più profonda di un semplice odore di codice.
Carl Manaster

1

In generale, mi piace molto "Arrange, Act, Assert" e lo uso come standard personale. L'unica cosa che non riesce a ricordarmi di fare, tuttavia, è disorganizzare ciò che ho organizzato quando le affermazioni sono state fatte. Nella maggior parte dei casi, questo non causa molto fastidio, poiché la maggior parte delle cose scompaiono automaticamente tramite la raccolta dei rifiuti, ecc. Se hai stabilito connessioni a risorse esterne, tuttavia, probabilmente vorrai chiudere quelle connessioni quando hai finito con le tue affermazioni o molti di voi hanno un server o una risorsa costosa là fuori da qualche parte aggrappandosi a connessioni o risorse vitali che dovrebbe essere in grado di cedere a qualcun altro. Questo è particolarmente importante se sei uno di quegli sviluppatori che non usa TearDown o TestFixtureTearDownpulire dopo uno o più test. Ovviamente "Arrange, Act, Assert" non è responsabile per la mia incapacità di chiudere ciò che apro; Ho solo menzionato questo "gotcha" perché non ho ancora trovato un buon sinonimo "A-word" per "dispose" da consigliare! Eventuali suggerimenti?


1
@carlmanaster, sei abbastanza vicino per me! Lo metto nel mio prossimo TestFixture per provarlo per le dimensioni. È come quel piccolo promemoria per fare ciò che tua madre avrebbe dovuto insegnarti: "Se lo apri chiudilo! Se lo sbagli, puliscilo!" Forse qualcun altro può migliorarlo, ma almeno inizia con una "a!" Grazie per il tuo suggerimento!
John Tobler

1
@carlmanaster, ho provato "Annul". È meglio di "smontaggio" e in un certo senso funziona, ma sto ancora cercando un'altra parola "A" che mi si fissi perfettamente in testa come "Arrange, Act, Assert". Forse "Annientare ?!"
John Tobler

1
Quindi ora ho "Arrange, Assume, Act, Assert, Annihilate". Hmmm! Sto complicando troppo le cose, eh? Forse farei meglio a baciarmi e tornare a "Arrange, Act, and Assert!"
John Tobler

1
Forse usare una R per il ripristino? So che non è una A, ma suona come un pirata che dice: Aaargh! e Reset fa rima con Assert: o
Marcel Valdez Orozco

1

Dai un'occhiata alla voce di Wikipedia su Design by Contract . L'Arrange-Act-Assert Holy Trinity è un tentativo di codificare alcuni degli stessi concetti e mira a dimostrare la correttezza del programma. Dall'articolo:

The notion of a contract extends down to the method/procedure level; the
contract for each method will normally contain the following pieces of
information:

    Acceptable and unacceptable input values or types, and their meanings
    Return values or types, and their meanings
    Error and exception condition values or types that can occur, and their meanings
    Side effects
    Preconditions
    Postconditions
    Invariants
    (more rarely) Performance guarantees, e.g. for time or space used

C'è un compromesso tra la quantità di impegno speso per la configurazione e il valore che aggiunge. AAA è un utile promemoria per i passaggi minimi richiesti ma non dovrebbe scoraggiare nessuno dal creare passaggi aggiuntivi.


0

Dipende dal tuo ambiente / linguaggio di test, ma di solito se qualcosa nella parte Arrange fallisce, viene lanciata un'eccezione e il test non riesce a visualizzarla invece di avviare la parte Act. Quindi no, di solito non uso una seconda parte Assert.

Inoltre, nel caso in cui la tua parte Arrange sia piuttosto complessa e non generi sempre un'eccezione, potresti forse considerare di avvolgerla in un metodo e scrivere un test per essa, in modo da essere sicuro che non fallirà (senza lanciare un'eccezione).


0

Non uso questo schema, perché penso che fare qualcosa come:

Arrange
Assert-Not
Act
Assert

Può essere inutile, perché presumibilmente sai che la tua parte Arrange funziona correttamente, il che significa che qualsiasi cosa sia nella parte Arrange deve essere testata anche o essere abbastanza semplice da non aver bisogno di test.

Usando l'esempio della tua risposta:

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7)); // <-- Pointless and against DRY if there 
                                    // are unit tests for Range(int, int)
    range.encompass(7);
    assertTrue(range.includes(7));
}

Temo che tu non capisca veramente la mia domanda. L'asserzione iniziale non riguarda il test di Arrange; è semplicemente garantire che l'Atto sia ciò che fa affermare lo stato alla fine.
Carl Manaster

E il mio punto è che, qualunque cosa tu abbia inserito nella parte Assert-Not, è già implicita nella parte Arrange, perché il codice nella parte Arrange è accuratamente testato e sai già cosa fa.
Marcel Valdez Orozco

Ma credo che ci sia valore nella parte Assert-Not, perché stai dicendo: dato che la parte Arrange lascia "il mondo" in "questo stato", il mio "Atto" lascerà "il mondo" in questo "nuovo stato" ; e se l'implementazione del codice da cui dipende la parte Arrange cambia, anche il test si interromperà. Ma ancora una volta, potrebbe essere contro DRY, perché (dovresti) anche avere test per qualsiasi codice tu stia dipendendo nella parte Arrange.
Marcel Valdez Orozco

Forse in progetti in cui ci sono più team (o un grande team) che lavorano sullo stesso progetto, una clausola del genere sarebbe piuttosto utile, altrimenti la trovo inutile e ridondante.
Marcel Valdez Orozco

Probabilmente una clausola del genere sarebbe migliore nei test di integrazione, test di sistema o test di accettazione, in cui la parte Arrange di solito dipende da più di un componente e ci sono più fattori che potrebbero causare un cambiamento inaspettato dello stato iniziale del "mondo". Ma non vedo posto per questo nei test unitari.
Marcel Valdez Orozco

0

Se vuoi davvero testare tutto nell'esempio, prova più test ... come:

public void testIncludes7() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
}

public void testIncludes5() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(5));
}

public void testIncludes0() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(0));
}

public void testEncompassInc7() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(7));
}

public void testEncompassInc5() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(5));
}

public void testEncompassInc0() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(0));
}

Perché altrimenti mancano così tante possibilità di errore ... ad es. Dopo encompass, l'intervallo include solo 7, ecc ... Ci sono anche test per la lunghezza dell'intervallo (per assicurarsi che non comprenda anche un valore casuale), e un'altra serie di test interamente per cercare di includere 5 nell'intervallo ... cosa ci aspetteremmo: un'eccezione in encompass o l'intervallo non viene modificato?

Ad ogni modo, il punto è che se ci sono dei presupposti nell'atto che vuoi testare, metterli nella loro prova, sì?


0

Io uso:

1. Setup
2. Act
3. Assert 
4. Teardown

Perché una configurazione pulita è molto importante.

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.