Daniel Kolman

Jak správně pojmenovat test

| 11 comments |

Na názvu testu záleží! Může se to zdát jako nepodstatná banalita, ale není. Správná metoda pojmenovávání testů zlepší čitelnost testů a pomáhá psát čistší kód. Názvy vašich testů prozrazují, jaký máte přístup k testování.

Vymýšlet názvy tříd a metod je těžké, dokonce tak těžké, že to zmiňuje i nejklasičtější anektoda o informatice: "There are only two hard problems in Computer Science: cache invalidation, naming things, and off-by-one errors." Když vám budou staří praktici tvrdit že "je to jen label" a že prý je důležitější obsah, nevěřte jim. Názvy tříd a metod jsou jako směrové tabule u silnice: Pokud jsou špatné, budete bloudit. Však se taky podívejte na jejich kód plný Helperů, Managerů, Executorů a Utilsů.

Existuje několik způsobů jak pojmenovat test:

Chaotické začátky

První test, který jste v životě napsali, se dost možná jmenoval test1. Je dost pravděpodobné, že druhý a třetí byl test2 a test3. Všichni jsme tak začínali. Na tom není nic špatného, pokud takové testy nakonec smažete. Někdo se ale v této fázi zasekne. V těžších případech to může dopadnout takhle:

public class ExecutorManagerTest {
    @Test 
    public void testWorks1() { ... }

    @Test 
    public void testFail() { ... }
}
Typickým znakem je, že název testovací metody neříká vůbec nic o tom, co se testuje. Obsah většinou nestojí za řeč - ještě jsem neviděl dobře napsaný test s podobným názvem.

Co metoda, to test

V dalším logickém stadiu skládáme název testu z názvu testované metody a předpony test:

public class StringTest {
    @Test 
    public void testIndexOf() { ... }

    @Test 
    public void testReplace() { ... }
}
Ze začátku to vypadá logicky, každou metodu chci přece otestovat. Jenže chování metod vykazuje více rysů než jeden a mění se podle hodnot vstupních parametrů, takže v testu často najdeme nejen víc assertů, ale někdy také více volání testované metody:
    @Test 
    public void testIndexOf() {
        // tfuj - do not use this!
        int i;

        i = "cukr kava limonada".indexOf("kava");
        assertEquals(5, i);

        i = "cukr kava limonada".indexOf("rum");
        assertEquals(-1, i);

        i = "kafe kafe kafe".indexOf("kafe", 3);
        assertEquals(5, i);
    }
Takové testy jsou dlouhé, špatně čitelné a na sobě závislé, protože jedna testovací metoda ve skutečnosti obsahuje několik testů. Lze se také setkat s jednou kuriózní variací, když existuje více přetížení jedné metody (česky overloadů). Pak lze vidět testy jako testIndexOfString() a testIndexOfStringInt().

Ano, v raných dobách testovacích se takto testy opravdu psaly. Je to ale způsob nemotorný, protože název testu nic neříká o tom, co se vlastně testuje. Je to způsob sporný, protože když budete striktní a budete vyžadovat ke každé metodě test, budete se muset nějak vypořádat s privátními metodami. Dokonce si myslím, že dříve rozšířený názor, že testovatelnost jde na úkor zapouzdření, byl způsoben právě zoufalou snahou napsat ke každé metodě, privátní nevyjímaje, test.

Formalizované metody

Název testu se dá také tvořit z nějaké šablony. Velmi známý je např. standard, se kterým přišel Roy Osherove, a používá šablonu UnitOfWork_StateUnderTest_ExpectedBehavior, volně přeloženo do češtiny CoTestuju_VychoziPodminky_OcekavaneChovaniNeboVysledek:

public class StringTest {
    @Test
    public void indexOf_containsSearchedString_returnsCorrectIndex() { ... }

    @Test
    public void indexOf_doesNotContainSearchedString_returnsMinusOne() { ... }
}
Tato metoda má několik výhod. Dává vám jasný návod, jak tvořit název testu. Díky tomu nemusíte pokaždé tápat, jednoduše použijete šablonu. Názvy testů jsou velmi konzistentní a pro toho, kdo zná šablonu, i lehce čitelné. Počítá se přitom s tím, že u jedné metody je potřeba otestovat několik vstupních stavů a různých chování, takže testy mohou být krátké, zaměřené na problém (focused) a v ideálním případě obsahují jen jeden assert.

Nevýhoda se skrývá v samotném principu této metody. Díky tomu, že nemusíte o názvu testu moc přemýšlet, vás nic nenutí přemýšlet ani o tom, co vlastně má testovaná třída dělat a jestli toho náhodou nedělá moc. Prostě bušíte testy pro všechny veřejné metody, jde vám to od ruky a nepřijde vám divný, že test má najednou 50 metod a 1000+ řádků.

Test jako živá dokumentace

Zastánci TDD tvrdí, že nejlepší dokumentací kódu jsou unit testy. Popisují způsob užití tříd, navíc je to dokumentace živá, která nemůže zastarat - v takovém případě by "spadl build" na continuous integration serveru (pokud nemáte CI server a nespouštíte testy po každém commitu, pak nechápu proč testy píšete). Jenže když se podíváte na testy "běžné kvality", bývá to dokumentace dost nečitelná. Jak to změnit?

Pište názvy testů tak, jako by to byl seznam fíčur testované třídy. Každý test je jako věta, která předpokládá název testované třídy jako podnět. Příklad:

public class ListTest {
    @Test 
    public void holdsItemsInTheOrderTheyWereAdded() { ... }

    @Test 
    public void canHoldMultipleReferencesToTheSameItem() { ... }

    @Test 
    public void throwsAnExceptionWhenRemovingAnItemItDoesntHold() { ... }
}
// Example from GOOS book
Výhodou této metody je čitelnost. Každý test se dá přečíst jako normální anglická věta: "List holds items in the order they were added". Stačí přečíst názvy testů abychom věděli, co má testovaná třída dělat. Každé IDE má příkaz "collapse all", který schová kód testů a nechá zobrazené jen názvy (v IDEA je to cmd shift -). Existují dokonce generátory, které z takto zapsaných testů vygenerují dokumentaci v HTML formátu.

Je tu ještě jedna, na první pohled méně zjevná výhoda. Když považujete test za seznam fíčur testované třídy, nutí vás to stále přemýšlet, co ještě je její zodpovědnost a co už ne. Jinými slovy, usnadňuje to dodržování single responsibility principle. Stačí názvům testů věnovat dostatečnou pozornost a ony vám samy napoví, kdy už toho třída dělá moc a měli byste se zamyslet, jak kód lépe rozdělit.

Tato konvence se jmenuje TestDox (někdy AgileDox). Díky tomu, že obrací vaši pozornost na design testované třídy, pomáhá psát čitelnější a udržovatelnější kód. Není náhoda, že novější testovací knihovny upřednostňují tento způsob. V javascriptu je to celkem běžná věc.

Závěrem: o prefixech

V Javě je zvykem, že všechny testovací metody začínají prefixem "test". Je to historický relikt starých verzí JUnitu z doby, kdy v Javě nebyly anotace. Nový JUnit (od verze 4) ani TestNG nic takového nevyžadují, tak to prosím nedělejte. Zvlášť ošklivě působí prefix test ve spojení s anotací: @Test public void testBlah(). Stejně tak se šíří zvyk používat prefix "should". Není problém si tímto slovem občas pomoct, ale vyžadovat to jako konvenci je podle mě zbytečné. Připomíná to Maďarskou notaci dávno zapomenuté minulosti.

(11) Comments

  1. alessio busta said...

    Should není maďarština. Vychází z BDD a mění imperativní kontext na deklarativní. To, že by něco mělo tak být, neznamená, že to tak je. A někdy je špatný samotný předpoklad. A should tohle pokrývá. Proto ho radějí opakovaně píšu, aby nebyla mylná očekávání

    17. února 2013 16:03
  2. Borek Bernard said...

    Dobrý článek. Z pohledu pojmenování a kategorizace je ještě zajímavá problematika sdružování testovacích metod do nějakých větších celků, kdy tyto větší celky nemusí nutně odpovídat třídám. Konvence nekončí na úrovni jednotlivých metod.

    A pak mě ještě zaujala jedna věc - při doporučení "té správné" konvence sis vybral "snadný" příklad, ale kdybys pokračoval se String#indexOf(), možná by ses dostal k některým problémům zmiňovaným výše. Pro tyto případy mi proto Osherovova konvence připadá OK.

    17. února 2013 20:26
  3. Daniel Kolman said...

    @Borek Ono by to šlo i se Stringem, ale ten příklad s Listem je tak dobrej, že mi přišlo škoda ho nevyužít:

    @Test public void returnsPositionOfSubstring() { ... }
    @Test public void returnsMinusOneInsteadOfPositionWhenSubstringIsNotFound() { ... }

    BTW pokud máš potřebu sdružovat testovací metody do větších celků, je to signál že testovaná třída je moc velká a zasloužila by rozdělit. Ale chápu že např. u controllerů v MVC je to konvence a dělá se to tak.

    @Alessio každá věta s "should" se dá přepsat, je to otázka vkusu a mně to v názvu testu přijde prostě zbytečný.

    17. února 2013 21:01
  4. Marian Schubert said...

    Damn computers! Nahled = Delete? :)

    Takze jeste jednou v rychlosti:

    Souhlas s Danielem - ale pokazde kdyz se snazim vyjadrit to co chci testovat nazvem metody tak placu. Pokud to jde tak preferuji testovaci frameworky ala:

    http://rspec.info
    https://metacpan.org/module/Test::Spec
    http://pivotal.github.com/jasmine/
    ...

    a pak misto returnsMinusOneInsteadOfPositionWhenSubstringIsNotFound psat:

    describe "SomeClass"
    describe ".indexOf"
    it "returns -1 when substring is not found"
    ...

    Existuje neco takoveho pro Javu/C#?

    18. února 2013 15:57
  5. Daniel Kolman said...

    V C# stojí za pozornost MSpec, kterej hodně zajímavym způsobem znásilňuje syntax C#:


    public class when_the_user_credentials_cannot_be_verified : ExampleSpecs {

    static Exception Exception;

    Because of = () =>
    Exception = Catch.Exception(() => SecurityService.Authenticate("user", "pass") );

    It should_fail = () =>
    Exception.ShouldBeOfType();
    }

    Ano toto je validní C# kód. https://github.com/machine/machine.specifications

    V Javě by možná šlo použít Spock nebo Jnario, ale nemám ani s jedním zkušenosti.

    Já osobně se kloním k tomu, že všechny BDD frameworky jsou vhodný spíš na více integrační testování (akceptační, end-to-end), moc mi nedává smysl použít to na unit test.

    18. února 2013 21:37
  6. Marian Schubert said...

    I na jednoduchem prikladu jako je treba String Calculator je podle mne rozdil vyrazny.

    https://gist.github.com/maio/4981608

    xyzUnit verze je pro mne i na takhle malem prikladu temer necitelny a hlavne nestrukturovany dump znaku. Zaroven obsahuje duplicity ve jmenech metod (prefixy ala delimiterMarker) a nektere veci dokonce nelze ani zapsat, protoze jsme omezeni tim co dovoluje jazyk jako jmeno metody.

    Aby to bylo jeste zabavnejsi tak nektere 'jazyky' (treba PL/SQL) maji i omezeni na delku nazvu metody (32 znaku)... ale to je jina story :)

    Dalsi vyhoda BDD je, ze i cloveku, ktery bezne pise testy toho prvniho typu ala "public void testIndexOf" prijde po chvilce divne psat

    describe "Something" { it "indexOf" { ... } }

    a misto indexOf tam napise o neco malo delsi pojednani o tom co to vlastne ma delat.

    19. února 2013 0:24
  7. alessio busta said...

    Jedinej framework, kterej potřebujete na BDD je váš mozek.

    19. února 2013 6:47
  8. Vít Kotačka said...

    Hezkej článek. Jak se stavíš ke code coverage? Podle mne může taky ovlivňovat design a pojmenování testů.

    21. února 2013 15:09
  9. Daniel Kolman said...

    @Vít To nechápu, jak by mohl coverage ovlivňovat desgin a pojmenování testů?

    22. února 2013 8:50
  10. Vít Kotačka said...

    @Daniel No, tím že se budu snažit o (maximální) pokrytí, tak by tam mohly působit dvě protichůdné síly:

    Pozitivní: dobrý design produkční třídy. Z mé zkušenosti vyplývá, že testy vedou ke kvalitnímu designu. Dokonce je to pro mne hlavní přínos unit testů. Čili víc testů, lepší design.

    Negativní: Testování pro testování, honba za procenty.

    A dále, pokud se někdo žene za coverage, tak to může být v rozporu s jinou konvencí/přístupem - třeba test jako dokumentace. Protože snaha o max. pokrytí bude nejspíš na úkor čitelnosti testů.

    25. února 2013 14:26
  11. theholyjava said...

    Výborný článek, díky! Bude to první český blogový příspěvek, který nedokážu vypustit z mého seznamu "Most interesting links of " [February '13], i když normálně tam dávám jen anglické věci. Jen tak dál!

    S pozdravem JH
    http://theholyjava.wordpress.com/

    27. února 2013 20:43

Leave a Response