it-swarm.it

È corretto avere più asserzioni in un test unitario?

Nel commento a questo fantastico post , Roy Osherove ha menzionato il progetto OAPT che è progettato per eseguire ogni asserzione in un singolo test.

Quanto segue è scritto nella home page del progetto:

I test unitari corretti dovrebbero fallire esattamente per una ragione, ecco perché dovresti usare un assert per unit test.

E, inoltre, Roy ha scritto nei commenti:

La mia linea guida è in genere quella di testare un CONCETTO logico per test. puoi avere più asserzioni sullo stesso oggetto. di solito saranno lo stesso concetto in fase di test.

Penso che ci siano alcuni casi in cui sono necessarie più asserzioni (ad es. Guard Assertion ), ma in generale cerco di evitarlo. Qual è la tua opinione? Fornisci un esempio reale in cui più asserzioni sono davvero necessarie.

430
Restuta

Non penso che sia necessariamente una cosa negativa , ma penso che dovremmo cercare di avere solo asserzioni 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 in realtà ha una sola affermazione. Penso che diventi solo un odore di codice (test?) quando hai circa cinque o più affermazioni nel tuo test.

Come si risolvono più asserzioni?

246
Jaco Pretorius

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

La chiave è che hai una sola azione e quindi controlli i risultati di quell'azione usando gli assert. Ma è "Disponi, agisci, asserisci, Fine del test". Se sei tentato di continuare il test eseguendo un'altra azione e più affermazioni in seguito, esegui invece un test separato.

Sono felice di vedere più dichiarazioni di asserzione che fanno parte del test della stessa azione. per esempio.

[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");
} 

Tu potresti combinali in una sola affermazione, ma è una cosa diversa dall'insistere che tu dovresti o devi. Non c'è alcun miglioramento nel combinarli.

per esempio. Il primo potrebbe essere

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

Ma questo 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ù) affermazioni in un'unica grande condizione booleana rende più difficile la lettura, più difficile da modificare e più difficile da capire perché non è riuscito. Perché farlo solo per il gusto di una regola?

[~ # ~] nb [~ # ~] : il codice che sto scrivendo qui è C # con NUnit, ma i principi valgono con altre lingue e quadri. Anche la sintassi può essere molto simile.

314
Anthony

Non ho mai pensato che più di un'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 condizioni complesse possano essere trasformate nel predicato previsto.

Sto testando solo un'unità (il metodo ToPredicate), ma sto coprendo tutto ciò a cui riesco a pensare nel test.

85
Matt Ellen

Quando utilizzo il test unitario per convalidare il comportamento di alto livello, inserisco assolutamente più asserzioni 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 poter essere sicuro che il codice si comporti come mi aspetto. Se una singola asserzione fallisce, non mi interessa che le restanti non vengano nemmeno messe in fuga; poiché lo stato del sistema non è più valido, quelle successive asserzioni non mi direbbero nulla di prezioso. * Se assertAllUnitsAlerting() fallisse, non saprei cosa fare di assertAllNotificationSent() success OR fallimento fino a quando non ho determinato la causa dell'errore precedente e l'ho corretto.

(* - Ok, potrebbero essere potenzialmente utili nel debug del problema. Ma le informazioni più importanti, che il test ha fallito, sono già state ricevute.)

21
BlairHippo

Un altro motivo per cui penso che più asserzioni in un metodo non sia negativo è descritto nel seguente codice:

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

Nel mio test voglio semplicemente provare che service.process() restituisce il numero corretto nelle istanze di classe Inner.

Invece di testare ...

@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() );
}
8
Betlista

Penso che ci siano molti casi in cui la scrittura di più asserzioni è valida all'interno della regola secondo cui un test dovrebbe fallire solo per un motivo.

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

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 ha esito negativo è a causa di un motivo, l'analisi non è corretta. Se sostenessi che questo test può fallire per tre diversi motivi, IMHO sarebbe troppo fine nella definizione di "un motivo".

6
Pete

Non conosco alcuna situazione in cui sarebbe una buona idea avere più asserzioni all'interno del metodo [Test] stesso. Il motivo principale per cui alle persone piace avere più asserzioni è che stanno cercando di avere una classe [TestFixture] per ogni classe sottoposta a test. Invece, puoi suddividere i test in più classi [TestFixture]. Ciò consente di vedere diversi modi in cui il codice potrebbe non aver reagito nel modo previsto, anziché solo quello in cui la prima asserzione ha avuto esito negativo. In questo modo hai almeno una directory per classe testata con molte classi [TestFixture] all'interno. Ogni classe [TestFixture] verrebbe nominata in base allo stato specifico di un oggetto che verrà testato. Il metodo [SetUp] porterà l'oggetto nello stato descritto dal nome della classe. Quindi hai più metodi [Test] che asseriscono cose diverse che ti aspetteresti essere vere, dato lo stato corrente dell'oggetto. Ogni metodo [Test] prende il nome dalla cosa che sta affermando, tranne forse potrebbe essere chiamato dopo il concetto anziché solo una 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 cosa stai testando e cosa ti aspetti solo guardando i nomi delle classi e dei metodi. Ciò si ridimensionerà anche quando inizi a realizzare tutti i piccoli casi Edge che desideri testare e quando trovi i bug.

Di solito ciò significa che l'ultima riga di codice all'interno del metodo [SetUp] deve memorizzare un valore di proprietà o un valore di ritorno in una variabile di istanza privata di [TestFixture]. Quindi potresti affermare più cose diverse su questa variabile di istanza da diversi metodi [Test]. Potresti anche fare affermazioni su quali diverse proprietà dell'oggetto sotto test sono impostate ora che si trova nello stato desiderato.

A volte è necessario formulare affermazioni lungo il percorso mentre si sta testando l'oggetto nello stato desiderato per assicurarsi che non si sia incasinato prima di riportare l'oggetto nello stato desiderato. In tal caso, tali asserzioni aggiuntive dovrebbero apparire all'interno del metodo [SetUp]. Se qualcosa va storto nel metodo [SetUp], sarà chiaro che qualcosa non ha funzionato nel test prima che l'oggetto raggiungesse lo stato desiderato che si intendeva testare.

Un altro problema che potresti incontrare è che potresti provare un'eccezione che ti aspetti di essere generata. Questo può indurti a non seguire il modello sopra. Tuttavia, può ancora essere ottenuto rilevando l'eccezione all'interno del metodo [SetUp] e memorizzandola in una variabile di istanza. Ciò ti consentirà di affermare cose diverse sull'eccezione, ognuna nel proprio metodo [Test]. È quindi possibile inoltre affermare altre cose sull'oggetto sottoposto a test per assicurarsi che non vi siano effetti collaterali indesiderati dovuti all'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));
        }
    }
}
3

Se hai più asserzioni in una singola funzione di test, mi aspetto che siano direttamente rilevanti per il test che stai conducendo. Per 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.
}

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

2
hagubear

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 affermazione fallisce. Con un'affermazione in ogni test è di solito più facile individuare ciò che è sbagliato.

Non riesco a pensare a uno scenario in cui più asserzioni sono davvero necessarie, poiché puoi sempre riscriverle come più condizioni nella stessa asserzione. Potrebbe essere preferibile, ad esempio, disporre di più passaggi per verificare i dati intermedi tra i passaggi anziché rischiare che i passaggi successivi si arrestino in modo anomalo a causa di un input errato.

2
Guffa

Se il test fallisce, non saprai se anche le seguenti affermazioni si interromperanno. Spesso, ciò significa che ti mancheranno preziose informazioni per capire l'origine del problema. La mia soluzione è usare un'asserzione ma con diversi valori:

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

Ciò mi consente di vedere tutte le asserzioni fallite contemporaneamente. Uso diverse righe perché la maggior parte degli IDE visualizzerà le differenze di stringa in una finestra di dialogo di confronto fianco a fianco.

2
Aaron Digulla

L'obiettivo del test unitario è di fornire quante più informazioni possibili su ciò che non funziona, ma anche di aiutare a individuare con precisione i problemi più importanti per primi. Quando sai logicamente che un'asserzione fallirà dato che un'altra asserzione fallisce o in altre parole c'è una relazione di dipendenza tra il test, allora ha senso farli rotolare come più asserzioni all'interno di un singolo test. Questo 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 esista, la preferenza sarebbe naturalmente quella di separare queste asserzioni in singoli test perché altrimenti la ricerca di 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 dover scrivere test eccessivamente complessi, ciò comporta un minore onere durante i test e probabilmente promuove una progettazione migliore.

1
jpierson

Sì, è ok avere più asserzioni purché un test fallito ti fornisca informazioni sufficienti per poter diagnosticare l'errore. Questo dipenderà da cosa stai testando e quali sono le modalità di errore.

I test unitari corretti dovrebbero fallire esattamente per una ragione, ecco perché dovresti usare un assert per unit test.

Non ho mai trovato tali formulazioni utili (che una classe dovrebbe avere un motivo per cambiare è un esempio di un adagio così inutile). Si consideri un'asserzione che due stringhe sono uguali, questo è semanticamente equivalente all'asserire che la lunghezza delle due stringhe è la stessa e che ogni carattere dell'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 una serie di asserzioni più piccole.

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

1
CurtainDog

Questa domanda è collegata al classico problema di bilanciamento tra il codice degli spaghetti e quello delle lasagne.

Avere più asserzioni potrebbe facilmente entrare nel problema degli spaghetti in cui non hai idea di cosa sia il test, ma avere una singola asserzione per test potrebbe rendere il tuo test ugualmente illeggibile avere più test in una grande lasagna rendendo la scoperta di quale test fa ciò che è impossibile .

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

0
gsf

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

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

Prendi il concetto logico di una transazione bancaria: nella maggior parte dei casi, prelevare un importo da un conto bancario DEVE includere l'aggiunta di tale importo a un altro conto. Non vuoi MAI separare queste due cose, formano un'unità atomica. Potresti voler fare due funzioni (ritira/aggiungiMoney) e quindi scrivere due diversi test unitari - in aggiunta. Ma queste due azioni devono aver luogo all'interno di una transazione e si desidera anche assicurarsi che la transazione funzioni. In tal caso, semplicemente non è sufficiente 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 unit test, in primo luogo, ma invece in un test di integrazione o accettazione. Ma quei confini sono fluidi, 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 i test unitari.

0
cslotty