it-swarm-eu.dev

Je v pořádku mít více testů v jednom testu jednotky?

V komentáři k tento skvělý příspěvek , Roy Osherove zmínil projekt OAPT , který je navržen tak, aby každý prosazoval jeden test.

Na domovskou stránku projektu je napsáno:

Správné jednotkové testy by neměly selhat z přesně jednoho důvodu , proto byste měli používat jedno tvrzení na jednotkový test.

A Roy také napsal v komentářích:

Mým vodítkem je obvykle to, že na test otestujete jeden logický koncept. můžete mít více tvrzení na stejném objektu. obvykle to bude stejný testovaný koncept.

Myslím, že v některých případech je třeba více tvrzení (např. Guard Assertion ), ale obecně se tomu snažím vyhnout. Jaký je váš názor? Uveďte příklad skutečného světa, ve kterém je více tvrzení opravdu potřeba.

430
Restuta

Nemyslím si, že je to nutně špatná věc , ale myslím, že bychom se měli usilovat o to, abychom měli pouze jediná tvrzení v našich testech. To znamená, že píšete mnohem více testů a naše testy by skončily testováním pouze jedné věci najednou.

Přesto bych řekl, že možná polovina mých testů má vlastně jen jedno tvrzení. Myslím, že se to stane kód (test?) Zápach , když máte v testu asi pět nebo více tvrzení.

Jak řešíte více tvrzení?

246
Jaco Pretorius

Testy by měly selhat pouze z jednoho důvodu, ale ne vždy to znamená, že by měl existovat pouze jeden příkaz Assert. IMHO je důležitější držet se vzoru „ spořádat, jednat, tvrdit “.

Klíčem je, že máte pouze jednu akci, a pak výsledky této akce zkontrolujete pomocí tvrzení. Ale je to „Uspořádat, jednat, tvrdit, konec testu “. Pokud jste v pokušení pokračovat v testování provedením další akce a poté více tvrzení, proveďte raději samostatný test.

Jsem rád, že mohu vidět několik tvrzení, která jsou součástí testování stejné akce. např.

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

nebo

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

Ty byste je mohli spojit do jednoho tvrzení, ale to je jiná věc než trvat na tom, abyste měli nebo musí. Jejich kombinací není žádné zlepšení.

např. První mohl být

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

Ale to není lepší - chybová zpráva z ní je méně specifická a nemá žádné další výhody. Jsem si jistý, že si můžete představit další příklady, kdy kombinace dvou nebo tří (nebo více) tvrzení do jedné velké booleovské podmínky ztěžuje čtení, těžší je změnit a těžší zjistit, proč selhala. Proč to jen kvůli pravidlu?

[~ # ~] nb [~ # ~] : Kód, který zde píšu, je C # s NUnit, ale zásady zůstanou u ostatních jazyků a rámce. Syntaxe může být také velmi podobná.

314
Anthony

Nikdy jsem si nemyslel, že více než jedno tvrzení bylo špatnou věcí.

Dělám to pořád:

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

Zde používám několik tvrzení, abych zajistil, že složité podmínky mohou být přeměněny na očekávaný predikát.

Testuji pouze jednu jednotku (metoda ToPredicate), ale zahrnuji vše, na co si v testu pomyslím.

85
Matt Ellen

Když používám testování jednotek k ověření chování na vysoké úrovni, do jediného testu jsem naprosto vložil několik tvrzení. Tady je test, který vlastně používám pro nouzový oznamovací kód. Kód, který se spouští před testem, uvede systém do stavu, kdy pokud se spustí hlavní procesor, spustí se alarm.

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

    pulseMainProcessor();

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

Představuje podmínky, které musí existovat v každém kroku procesu, abych si byl jistý, že se kód chová tak, jak očekávám. Pokud selže jediné tvrzení, nestarám se o to, že zbývající nebudou ani běžet; protože stav systému již není platný, tato následná tvrzení by mi neřekly nic hodnotného. * Pokud by selhala assertAllUnitsAlerting(), pak bych nevěděla, co dělat s assertAllNotificationSent() s úspěchem OR, dokud jsem nerozhodl co způsobilo předchozí chybu a napravilo ji.

(* - Dobře, možná by mohli být užitečné při ladění problému. Ale nejdůležitější informace, že test selhal, již byly obdrženy.)

21
BlairHippo

Další důvod, proč si myslím, že vícenásobná tvrzení v jedné metodě není špatná, je popsána v následujícím kódu:

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

V mém testu prostě chci vyzkoušet, že service.process() vrátí správné číslo v instancích třídy Inner.

Místo testování ...

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

Dělám

@Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
8
Betlista

Myslím, že existuje spousta případů, kdy je psaní více tvrzení platné v rámci pravidla, že test by měl selhat pouze z jednoho důvodu.

Představte si například funkci, která analyzuje řetězec 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);
}

Pokud test selže, je to z jednoho důvodu, analýza je nesprávná. Pokud byste tvrdili, že tento test může selhat ze tří různých důvodů, byl byste IMHO příliš jemnozrnný ve své definici „jednoho důvodu“.

6
Pete

Nevím o žádné situaci, kdy by bylo dobré mít v rámci samotné metody [Test] několik tvrzení. Hlavním důvodem, proč lidé rádi mají více Asercí, je to, že se snaží mít jednu třídu [TestFixture] pro každou testovanou třídu. Místo toho můžete své testy rozdělit do více tříd [TestFixture]. To vám umožní zobrazit více způsobů, jak kód nemusel reagovat tak, jak jste očekávali, namísto jen těch, u nichž první tvrzení selhalo. Způsob, jak toho dosáhnete, je, že ve své třídě bude testován alespoň jeden adresář se spoustou tříd [TestFixture]. Každá třída [TestFixture] bude pojmenována podle konkrétního stavu objektu, který budete testovat. Metoda [SetUp] převede objekt do stavu popsaného názvem třídy. Pak máte více metod [Test], z nichž každá tvrdí různé věci, které byste očekávali, že budou pravdivé, vzhledem k aktuálnímu stavu objektu. Každá metoda [Test] je pojmenována po věci, kterou uplatňuje, s výjimkou snad, že by mohla být pojmenována po konceptu místo pouhého anglického odečtu kódu. Pak každá implementace metody [Test] potřebuje pouze jediný řádek kódu, kde něco prosazuje. Další výhodou tohoto přístupu je to, že testy jsou velmi čitelné, protože je zcela jasné, co testujete a co očekáváte pouhým pohledem na názvy tříd a metod. To se také lépe přizpůsobí, jakmile začnete realizovat všechny malé případy Edge, které chcete testovat, a jak najdete chyby.

Obvykle to znamená, že poslední řádek kódu uvnitř metody [SetUp] by měl ukládat hodnotu vlastnosti nebo návratovou hodnotu v proměnné soukromé instance [TestFixture]. Pak můžete uplatnit několik různých věcí o této proměnné instance z různých metod [Test]. Můžete také učinit tvrzení o tom, jaké různé vlastnosti testovaného objektu jsou nastaveny na nyní, když je v požadovaném stavu.

Někdy je třeba učinit tvrzení podél cesty, jak se dostat testovaný objekt do požadovaného stavu, aby se ujistil, že jste se nepořádek před dostat objekt do požadovaného stavu. V takovém případě by se tato další tvrzení měla objevit uvnitř metody [SetUp]. Pokud se v metodě [SetUp] něco pokazí, bude jasné, že se s testem něco stalo, než se objekt někdy dostal do požadovaného stavu, který jste chtěli otestovat.

Dalším problémem, na který se můžete setkat, je testování výjimky, kterou jste očekávali. To vás může přimět k tomu, abyste tento model nesledovali. Toho však lze stále dosáhnout zachycením výjimky uvnitř metody [SetUp] a jejím uložením do proměnné instance. To vám umožní uplatnit na výjimku různé věci, každá ve své vlastní [Testovací] metodě. Potom můžete také uplatnit na testovaný objekt další věci, abyste se ujistili, že nebyly vyvolány žádné nezamýšlené vedlejší účinky.

Příklad (to by bylo rozděleno do více souborů):

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

Pokud máte v jedné testovací funkci více tvrzení, očekávám, že budou přímo relevantní pro test, který provádíte. Například,

@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.
}

Mít spoustu testů (i když máte pocit, že je to pravděpodobně nadměrné množství), není špatná věc. Můžete tvrdit, že důležitější a nejdůležitější testy jsou důležitější. Takže, když uplatňujete tvrzení, ujistěte se, že tvrzení tvrzení jsou správná, místo aby se starali o mnoho tvrzení příliš. Pokud potřebujete více než jednu, použijte více než jednu.

2
hagubear

Mít více tvrzení ve stejném testu je pouze problém, když test selže. Možná budete muset ladit test nebo analyzovat výjimku a zjistit, jaké tvrzení selže. S jedním tvrzením v každém testu je obvykle snazší určit, co je špatně.

Nedokážu si představit scénář, ve kterém bude více tvrzení skutečně potřeba, protože je můžete vždy přepsat jako vícenásobné podmínky ve stejném tvrzení. Může však být výhodné, pokud například máte několik kroků k ověření mezilehlých dat mezi kroky, spíše než riskováte, že v důsledku špatného zadání dojde k selhání pozdějších kroků.

2
Guffa

Pokud váš test selže, nebudete vědět, zda se zlomí i následující tvrzení. To často znamená, že vám budou chybět cenné informace, aby zjistily zdroj problému. Mým řešením je použít jedno tvrzení, ale s několika hodnotami:

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

To mi umožňuje vidět všechna neúspěšná tvrzení najednou. Používám několik řádků, protože většina IDE zobrazí rozdíly řetězců v dialogu pro porovnání vedle sebe.

2
Aaron Digulla

Cílem testu jednotky je poskytnout co nejvíce informací o tom, co selhává, ale také nejprve pomoci přesně určit nejzákladnější problémy. Pokud logicky víte, že jedno tvrzení selže vzhledem k tomu, že jiné tvrzení selže, nebo jinými slovy, existuje závislostní vztah mezi testem, pak má smysl je hodit jako více tvrzení v rámci jednoho testu. Výhoda spočívá v tom, že výsledky testů se nevyhýbají zjevným selháním, která by mohla být odstraněna, pokud bychom v rámci jednoho testu zachránili první tvrzení. V případě, že tento vztah neexistuje, bylo by přirozeně preferováno oddělit tato tvrzení do jednotlivých testů, protože v opačném případě by zjištění těchto selhání vyžadovalo více iterací testovacích běhů, aby se vyřešily všechny problémy.

Pokud pak také navrhujete jednotky/třídy takovým způsobem, že by bylo nutné napsat příliš složité testy, je to při testování menší zátěže a pravděpodobně podporuje lepší design.

1
jpierson

Ano, je v pořádku mít více tvrzení pokud selhávající test vám poskytne dostatek informací, abyste mohli diagnostikovat selhání. To bude záviset na tom, co testujete a jaké jsou režimy selhání.

Správné jednotkové testy by neměly selhat přesně z jednoho důvodu, proto byste měli používat jeden test na jednotku.

Nikdy jsem nepovažoval takové formulace za užitečné (že třída by měla mít jeden důvod ke změně, je příkladem takového neužitečného přísloví). Uvažujme o tvrzení, že dva řetězce jsou stejné, je to sémanticky ekvivalentní k tvrzení, že délka obou řetězců je stejná a každý znak v odpovídajícím indexu je stejný.

Mohli bychom zobecnit a říci, že jakýkoli systém vícenásobných tvrzení by mohl být přepsán jako jediné tvrzení, a jakékoli jedno tvrzení by mohlo být rozloženo na soubor menších tvrzení.

Zaměřte se tedy pouze na srozumitelnost kódu a na jasnost výsledků testu a nechte to, aby určovalo počet tvrzení, která používáte, a ne naopak.

1
CurtainDog

Tato otázka souvisí s klasickým problémem vyvažování mezi problémy se špagetami a lasagneovými kódy.

S více tvrzeními by se snadno mohlo dostat do problému se špagetami, kde nemáte představu o tom, o čem je test, ale mít jediný tvrzení za test by mohlo způsobit, že vaše testování bude stejně nečitelné, že bude mít několik testů ve velkém laseru, což zjišťuje, který test dělá to nemožné .

Existují některé výjimky, ale v tomto případě je odpovědí udržení kyvadla uprostřed.

0
gsf

Odpověď je velmi jednoduchá - pokud otestujete funkci, která mění více než jeden atribut, stejného objektu nebo dokonce dvou různých objektů, a správnost funkce závisí na výsledcích všech těchto změn, pak chcete uplatnit že každá z těchto změn byla provedena správně!

Představuji si logický koncept, ale zpětný závěr by řekl, že žádná funkce nesmí nikdy změnit více než jeden objekt. Ale podle mých zkušeností to není možné implementovat ve všech případech.

Vezměte logický koncept bankovní transakce - výběr částky z jednoho bankovního účtu ve většině případů MUSÍ zahrnovat přidání této částky na jiný účet. NIKDY nechcete oddělit tyto dvě věci, tvoří atomovou jednotku. Možná budete chtít provést dvě funkce (odebrat/přidatMoney) a tak napsat dvě různé testy jednotek - navíc. Tyto dvě akce se však musí uskutečnit v rámci jedné transakce a také se chcete ujistit, že transakce funguje. V takovém případě prostě nestačí, aby se ujistil, že jednotlivé kroky byly úspěšné. V testu musíte zkontrolovat oba bankovní účty.

Mohou existovat složitější příklady, které byste nejprve netestovali v testu jednotky, ale místo toho v integračním nebo akceptačním testu. Ale tyto hranice jsou plynulé, IMHO! Není tak snadné se rozhodnout, je to otázka okolností a možná osobní preference. Výběr peněz z jednoho účtu a jejich přidání na jiný účet je stále velmi jednoduchá funkce a určitě je kandidátem na testování jednotek.

0
cslotty