Va bene avere più asserzioni in un singolo test unitario?

343

Nel commento a questo bel post , Roy Osherove ha menzionato il progetto OAPT progettato per eseguire ogni assert in un singolo test.

Quanto segue è scritto sulla home page del progetto:

Proper unit tests should fail for exactly one reason, that’s why you should be using one assert per unit test.

E, anche, Roy ha scritto nei commenti:

My guideline is usually that you test one logical CONCEPT per test. you can have multiple asserts on the same object. they will usually be the same concept being tested.

Penso che ci siano alcuni casi in cui sono necessarie più asserzioni (ad es. Asserzione di guardie ), ma in generale io cerca di evitare questo. Qual è la tua opinione? Fornisci un esempio di parola reale in cui più affermazioni sono necessarie .

    
posta Restuta 02.06.2016 - 11:12
fonte

15 risposte

210

Non penso che sia necessariamente una brutta cosa , ma penso che dovremmo cercare di avere solo affermazioni singole nei nostri test. Ciò significa che scrivi molti più test e i nostri test finirebbero per testare solo una cosa alla volta.

Detto questo, direi che forse metà dei miei test ha solo una affermazione. Penso che diventi un codice (test?) Olfattivo quando hai circa cinque o più affermazioni nel tuo test.

Come risolvi più asserzioni?

    
risposta data 12.05.2015 - 15:03
fonte
242

I test dovrebbero fallire solo per una ragione, ma ciò non significa sempre che dovrebbe esserci solo una dichiarazione Assert . IMHO è più importante attenersi al modello " Disponi, agisci, asserisci ".

La chiave è che hai una sola azione, e poi controlli i risultati di quell'azione usando le asserzioni. Ma è "Arrange, Act, Assert, End of test ". Se sei tentato di continuare il test eseguendo un'altra azione e più asserti in seguito, fai invece un test separato.

Sono felice di vedere più affermazioni di affermazione che formano parti di test della stessa azione. per es.

[Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 

o

[Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 

Puoi potresti combinarli in un'unica asserzione, ma è una cosa diversa dall'insistere che dovresti o devi . Non c'è alcun miglioramento dalla combinazione di essi.

es. Il primo potrebbe essere

Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 

Ma non è meglio - il messaggio di errore è meno specifico e non ha altri vantaggi. Sono sicuro che puoi pensare ad altri esempi in cui combinare due o tre (o più) asserzioni in un'unica grande situazione booleana rende più difficile la lettura, più difficile da modificare e più difficile capire perché ha fallito. Perché farlo solo per il gusto di una regola?

NB : il codice che sto scrivendo qui è C # con NUnit, ma i principi si terranno con altri linguaggi e framework. Anche la sintassi potrebbe essere molto simile.

    
risposta data 12.05.2015 - 15:04
fonte
83

Non ho mai pensato che più di una affermazione fosse una brutta cosa.

Lo faccio tutto il tempo:

public void ToPredicateTest()
{
    ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100);
    Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400)
                                                       .Or()
                                                       .OpenParenthesis()
                                                       .GreaterThanConst(500)
                                                       .And()
                                                       .LessThanConst(1000)
                                                       .And().Not()
                                                       .EqualsConst(666)
                                                       .CloseParenthesis()
                                                       .ToPredicate();
    Assert.IsTrue(p(ResultField.FillResult(rf, 399)));
    Assert.IsTrue(p(ResultField.FillResult(rf, 567)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 400)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 666)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 1001)));

    Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate();

    Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true)));
    Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false)));
}

Qui utilizzo più asserzioni per assicurarmi che le condizioni complesse possano essere trasformate nel predicato previsto.

Sto solo testando una unità (il metodo ToPredicate ), ma sto trattando tutto quello che posso pensare nel test.

    
risposta data 23.03.2014 - 08:20
fonte
18

Quando utilizzo i test unitari per convalidare il comportamento di alto livello, inserisco assolutamente più affermazioni in un singolo test. Ecco un test che sto effettivamente utilizzando per un codice di notifica di emergenza. Il codice che viene eseguito prima del test mette il sistema in uno stato in cui, se viene eseguito il processore principale, viene inviato un allarme.

@Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}

Rappresenta le condizioni che devono esistere in ogni fase del processo per farmi avere la certezza che il codice si comporti nel modo in cui mi aspetto. Se una singola asserzione fallisce, non mi interessa che i rimanenti non vengano nemmeno messi in moto; poiché lo stato del sistema non è più valido, quelle successive non mi direbbero nulla di valido. * Se assertAllUnitsAlerting() non è riuscito, non saprei cosa fare del successo o fallimento di assertAllNotificationSent() fino a quando non ho determinato che cosa stava causando l'errore precedente e lo ha corretto.

(* - Ok, potrebbero essere utili nel debugging del problema, ma l'informazione più importante, che il test ha fallito, è già stata ricevuta.)

    
risposta data 12.05.2015 - 15:01
fonte
8

Un altro motivo per cui penso che affermazioni multiple in un metodo non sia una cosa negativa è descritto nel seguente codice:

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

Nel mio test voglio semplicemente verificare che service.process() restituisca il numero corretto in Inner istanze di classe.

Invece di provare ...

@Test
public void test() {
    Result res = service.process();
    if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() );
}

Sto facendo

@Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
    
risposta data 30.10.2012 - 15:21
fonte
6

Penso che ci siano molti casi in cui scrivere affermazioni multiple è valido nella regola secondo cui un test dovrebbe fallire solo per una ragione.

Ad esempio, immagina una funzione che analizza una stringa di data:

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}

Se il test fallisce è per una ragione, l'analisi non è corretta. Se sosteresti che questo test può fallire per tre diversi motivi, potresti IMHO essere troppo granuloso nella tua definizione di "una ragione".

    
risposta data 02.06.2016 - 13:00
fonte
2

Avere più asserzioni nello stesso test è solo un problema quando il test fallisce. Quindi potrebbe essere necessario eseguire il debug del test o analizzare l'eccezione per scoprire quale asserzione è fallita. Con una sola affermazione in ciascun test, solitamente è più facile individuare cosa non va.

Non riesco a pensare a uno scenario in cui più asserzioni sono veramente necessarie , poiché puoi sempre riscriverle come condizioni multiple nella stessa asserzione. Potrebbe tuttavia essere preferibile se, ad esempio, hai diversi passaggi per verificare i dati intermedi tra i passaggi piuttosto che rischiare che i passaggi successivi si interrompano a causa di input non validi.

    
risposta data 28.09.2010 - 16:05
fonte
2

Se il tuo test fallisce, non saprai se anche le seguenti asserzioni si romperanno. Spesso, questo significa che mancheranno informazioni preziose per capire la fonte del problema. La mia soluzione è usare un assert ma con diversi valori:

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);

Questo mi consente di vedere tutte le asserzioni fallite contemporaneamente. Io uso diverse righe perché la maggior parte degli IDE visualizzeranno differenze di stringhe in una finestra di dialogo di confronto side-by-side.

    
risposta data 28.09.2010 - 17:22
fonte
2

Se hai più affermazioni in una singola funzione di test, mi aspetto che siano direttamente pertinenti al test che stai conducendo. Ad esempio,

@Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}

Avere molti test (anche quando ritieni che sia probabilmente un eccesso) non è una brutta cosa. Puoi obiettare che avere i test vitali e più essenziali è più importante. Quindi, quando stai affermando, assicurati che le tue affermazioni siano posizionate correttamente piuttosto che preoccuparti di più asserzioni troppo. Se ne hai bisogno più di uno, usa più di uno.

    
risposta data 12.05.2015 - 15:01
fonte
2

Non conosco nessuna situazione in cui sarebbe una buona idea avere più asserzioni all'interno del metodo [Test] stesso. Il motivo principale per cui alla gente piace avere più asserzioni è che stanno cercando di avere una classe [TestFixture] per ogni classe sottoposta a test. Invece, puoi suddividere i tuoi test in più classi [TestFixture]. Ciò ti consente di vedere diversi modi in cui il codice potrebbe non aver reagito nel modo previsto, invece di quello in cui la prima asserzione non è riuscita. Il modo in cui ottieni questo è che hai almeno una directory per classe in fase di test con molte classi [TestFixture] all'interno. Ogni classe [TestFixture] verrebbe chiamata dopo lo stato specifico di un oggetto che si testerà. Il metodo [SetUp] porterà l'oggetto nello stato descritto dal nome della classe. Quindi hai più metodi [Test] ognuno che asserisce cose diverse che ti aspetteresti siano vere, dato lo stato attuale dell'oggetto. Ogni metodo [Test] prende il nome dalla cosa che sta asserendo, tranne forse potrebbe essere chiamato dopo il concetto invece di una semplice lettura inglese del codice. Quindi, ogni implementazione del metodo [Test] richiede solo una singola riga di codice in cui sta affermando qualcosa. Un altro vantaggio di questo approccio è che rende i test molto leggibili in quanto diventa abbastanza chiaro ciò che si sta testando e ciò che si aspetta semplicemente guardando i nomi di classi e metodi. Anche questo scalerà meglio quando inizi a realizzare tutti i piccoli casi limite che vuoi testare e come trovi bug.

Di solito questo significa che l'ultima riga di codice all'interno del metodo [SetUp] dovrebbe memorizzare un valore di proprietà o restituire un valore in una variabile di istanza privata di [TestFixture]. Quindi potresti affermare più cose diverse su questa variabile d'istanza da diversi metodi [Test]. Puoi anche fare affermazioni su quali diverse proprietà dell'oggetto in prova sono impostate ora che è nello stato desiderato.

A volte è necessario fare affermazioni lungo il percorso mentre si sta ottenendo l'oggetto in prova nello stato desiderato per assicurarsi di non aver rovinato prima di portare l'oggetto nello stato desiderato. In quel caso, queste ulteriori affermazioni dovrebbero apparire nel metodo [SetUp]. Se qualcosa va storto nel metodo [SetUp], sarà chiaro che qualcosa non ha funzionato con il test prima che l'oggetto abbia raggiunto lo stato desiderato che intendevi testare.

Un altro problema che potresti incontrare è che potresti provare un'eccezione che ti aspettavi di essere lanciata. Questo può indurti a non seguire il modello di cui sopra. Tuttavia, può comunque essere ottenuto catturando l'eccezione nel metodo [SetUp] e memorizzandola in una variabile di istanza. Ciò ti consentirà di affermare diverse cose sull'eccezione, ciascuna nel suo metodo [Test]. Puoi anche affermare altre cose sull'oggetto sottoposto a test per assicurarti che non vi siano effetti collaterali indesiderati dall'eccezione generata.

Esempio (questo sarebbe suddiviso in più file):

namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
    
risposta data 12.05.2015 - 15:06
fonte
1

L'obiettivo del test unitario è di darti quante più informazioni possibili su ciò che sta fallendo, ma anche di aiutare a individuare con precisione i problemi più fondamentali. Quando sai logicamente che un'affermazione fallirà dato che un'altra asserzione fallisce o in altre parole c'è una relazione di dipendenza tra il test, allora ha senso riportarli come affermazioni multiple all'interno di un singolo test. Ciò ha il vantaggio di non sporcare i risultati del test con evidenti fallimenti che avrebbero potuto essere eliminati se avessimo salvato la prima asserzione all'interno di un singolo test. Nel caso in cui questa relazione non esistesse, la preferenza sarebbe naturalmente quella di separare queste asserzioni in singoli test perché altrimenti trovare questi errori richiederebbe più iterazioni di esecuzioni di test per risolvere tutti i problemi.

Se poi si progettano anche le unità / classi in modo tale da richiedere la scrittura di test eccessivamente complessi, ciò rende meno oneroso durante i test e probabilmente promuove un design migliore.

    
risposta data 15.03.2013 - 23:00
fonte
0

Sì, è ok avere più asserzioni fino a quando un test fallito ti dà abbastanza informazioni per essere in grado di diagnosticare l'errore. Ciò dipenderà da cosa stai testando e da quali sono le modalità di errore.

Proper unit tests should fail for exactly one reason, that’s why you should be using one assert per unit test.

Non ho mai trovato che tali formulazioni siano utili (che una classe dovrebbe avere una ragione per cambiare è un esempio di tale adagio non utile). Considera un'affermazione secondo cui due stringhe sono uguali, ciò equivale semanticamente ad affermare che la lunghezza delle due stringhe è la stessa e ciascun carattere nell'indice corrispondente è uguale.

Potremmo generalizzare e dire che qualsiasi sistema di asserzioni multiple potrebbe essere riscritto come una singola asserzione, e ogni singola asserzione potrebbe essere scomposta in un insieme di asserzioni più piccole.

Quindi concentrati solo sulla chiarezza del codice e sulla chiarezza dei risultati del test, e lascia che indichi il numero di asserzioni che usi piuttosto che viceversa.

    
risposta data 19.04.2018 - 06:48
fonte
0

La risposta è molto semplice: se provi una funzione che cambia più di un attributo, dello stesso oggetto o anche di due oggetti diversi e la correttezza della funzione dipende dai risultati di tutte queste modifiche, allora vuoi affermare che ognuna di queste modifiche è stata eseguita correttamente!

Ho l'idea di un concetto logico, ma la conclusione inversa direbbe che nessuna funzione deve mai cambiare più di un oggetto. Ma è impossibile da implementare in tutti i casi, nella mia esperienza.

Prendi il concetto logico di una transazione bancaria - il prelievo di un importo da un conto bancario nella maggior parte dei casi DEVE includere l'aggiunta di tale importo a un altro account. Non vuoi MAI separare queste due cose, esse formano un'unità atomica. Potresti voler fare due funzioni (prelevare / aggiungere denaro) e quindi scrivere due diversi test di unità - in aggiunta. Ma queste due azioni devono svolgersi all'interno di una transazione e si desidera anche assicurarsi che la transazione funzioni. In tal caso, semplicemente non è sufficiente per assicurarsi che i singoli passaggi abbiano avuto successo. Devi controllare entrambi i conti bancari, nel tuo test.

Potrebbero esserci esempi più complessi che non verrebbero testati in un test di unità, in primo luogo, ma in un test di integrazione o di accettazione. Ma quei confini sono fluenti, IMHO! Non è così facile decidere, è una questione di circostanze e forse di preferenze personali. Prelevare denaro da uno e aggiungerlo a un altro account è ancora una funzione molto semplice e sicuramente un candidato per il test dell'unità.

    
risposta data 12.12.2018 - 16:45
fonte
-1

Questa domanda è legata al classico problema del bilanciamento tra spaghetti e codici a lasagna.

Avere più assertimenti potrebbe facilmente entrare nel problema degli spaghetti dove non hai idea di cosa sia il test, ma avere un singolo assert per test potrebbe rendere il tuo test ugualmente illeggibile avere più test in una grande lasagna che fa la scoperta quale test fa cosa impossibile.

Ci sono alcune eccezioni, ma in questo caso mantenere il pendolo nel mezzo è la risposta.

    
risposta data 11.06.2016 - 00:22
fonte
-3

Non sono nemmeno d'accordo con il "fallire per una sola ragione" in generale. Ciò che è più importante è che i test sono brevi e si legge chiaramente.

Tuttavia, ciò non è sempre realizzabile e quando un test è complicato, un nome descrittivo (lungo) e un test su un numero inferiore di cose hanno più senso.

    
risposta data 13.04.2014 - 00:10
fonte

Leggi altre domande sui tag