Unit testy a čistota návrhu

I když si myslím, že mám s unit testy dost zkušeností, stále ještě mě dokáží dost překvapit. Kromě toho, že se překvapuji tím jak často je „zapomínám“ psát, překvapují mě většinou pozitivně. Zrovna nedávno mě překvapily znovu.

Psal jsem program, který mimo jiné prohledává adresář na disku, poté nalezené soubory zpracovává, přesouvá jinam, archivuje, vytváří adresáře a dělá jiné psí kusy se souborovým systémem. Tuto část jsem chtěl otestovat. Ale jak na to? Jedno z dogmat unit testů nám říká, že unit test nepracuje se souborovým systémem. Kdybych použil třídu java.io.File, nemohl bych napsat unit test, protože tato třída na disk přistupuje. Řešení je jednoduché – musel jsem abstrahovat přístup na souborový systém. Přiznávám, že se mi do toho moc nechtělo. Abstrahovat přístup, který je už abstrahován jazykem mi přišlo zbytečné. Ale nakonec jsem se odhodlal a napsal jsem si rozhraní FileSystemDao



public interface FileSystemDao {
  
  public void copyFile(File source, File destinationthrows IOException;
  public void moveFile(File source, File destinationthrows IOException;
  public File[] listFiles(File directory, String pattern);
  public boolean isDirAccessible(File directory);
  public boolean isAccessible(File file);
  public File findOldest(File[] files);
  public boolean createDir(File file);
  public boolean delete(File file);
  public InputStream getInputStream(File filethrows IOException;
  public Reader getReader(File filethrows IOException;
}

Uznávám, že nazývat to DAO je trochu zavádějící, ale podle mě to vystihuje co jsem jím chtěl vyjádřit.

Skvělé, mám DAO pro přístup do souborového systému, mohu si vytvořit mock a vesele unit testovat. Nápad je to dobrý, nicméně má své vady na kráse. Výsledný test je neuvěřitelně nepřehledný. Vytvořit mock, který simuluje stav souborového systému před operací a po ní je docela náročné. Navíc jsem přišel na to, že tento test je až moc odtržen od reality. Neupozorní nás na to, že například kopírujeme soubor do neexistujícího adresáře. Při každé změně aplikace, se navíc testy musely dost pracně aktualizovat. Vydržel jsem to den a pak jsem tyto testy smazal. Kvůli pracnosti jsem zavrhl i myšlenku na to, že bych souborový systém simuloval v paměti, například pomocí nějakého stromu. Poslechl jsem Testuvia, který říká

Nezabředni do unit testového dogmatu

Napsal jsem normální funkční test, který využívá souborový systém. S tímto testem jsem nadmíru spokojen. Probíhá velmi rychle, hledá mi chyby a není s ním žádná práce. Navíc když se na něj podívám, krásně popisuje to co má testovaný kus kódu dělat.

Ale co se stalo s rozhraním FileSystemDao? Vypadá to, že teď už není potřeba. Opak je pravdou. Hodně se osvědčilo. Je v něm kód pracující se souborovým systémem soustředěný na jediném místě. Když jsem byl donucen změnit kód pro vyhledávání souborů pomocí masky, změnil jsem ho na jediném místě. Když se přišlo na to, že před každým přesouváním souboru musím vytvořit cílový adresář, zařídil jsem to změnou na jediném místě. Když se ukázalo, že je toto řešení neefektivní, opravil jsem to pouze v implementaci tohoto rozhraní.

Vidíme tedy, že mě potřeba napsat unit test donutila abstrahovat něco co by mě vůbec abstrahovat nenapadlo. Ono totiž na první pohled není o moc lepší psát fileSystemDao.moveFile(source, dest) než source.renameTo(dest). Teprve čas ukázal, že ta první, na první pohled nesmyslná varianta má své výhody. Nezbývá mi než si zopakovat starou pravdu

Unit testy si vynucují dobrý návrh.

6 Responses to “Unit testy a čistota návrhu”

  1. Honza Novotný Says:

    Zlatá pravda - testy se vyplatí i jen ve chvíli kdy se kód vyvíjí - už v tom momentě začínají pomáhat čistit návrh. A druhá zlatá pravda - s mocky se to nedá přehánět, jsou sice řádově mnohonásobně rychlejší, ale nejsou tak spolehlivé jako integrační testy. Na druhou stranu nejsou tak náchylné na změny. No prostě jak říká Testuvius "Sometimes your thirst is best quenched by beer and your hunger by buffallo wings."

  2. Petr Jůza Says:

    Co se týká Mock testování, tak se musím přiznat, že pořád si nejsem 100% jistý, že ten čas věnovaný Mock testů byl dobře využitý.

    Co tím mám na mysli? Mock testy mi přijdou celkem náročné na údržbu - během vývoje se určité věci celkem mění, navíc používám JMock, kde se zapisuje jméno metody pomocí řetězce, takže zde není možné použít refactoring.
    To samo o sobě mi ještě tak nevadí, kdybych ovšem viděl pořadný přínos těchto testů. Jako jedinou hmatatelnou výhodu vidím v tom, že mi ty Mock testy udržují správné hranice uvnitř systému. Jinak řečeno díky těm testům se mohu spolehnout, že jiní členové týmu mi tam něco nezměnili, že to pořád funguje stejně jak má.
    A zde si právě nejsem jistý, zda to není málo za ten čas strávený psaním těchto testů, zda by nebylo lepší tento čas investovat do jiných typů testů, např. testování GUI?

    Jen pro info: vyvijíme v rámci 2-3 členého týmu, nepoužíváme TDD, testy píšeme na úrovni rozhraních (mezi jednotlivými vrstvami aplikace) a pak funkční testy dle potřeby.

  3. Lukáš Křečan Says:

    Já používám EasyMock s kterým jsem spokojen. A mám takovou zkušenost, že když se něco špatně testuje, tak je to špatně navrženo. Neplatí to vždy, ale často je to dobrý indikátor příliš úzké vazby mezi třídami nebo toho, že jedna třída toho dělá víc než by měla.

  4. Petr Jůza Says:

    Naprosto s tebou souhlasim, ze na testech se hodne dobre pozna, zda produkcni kod je dobre navrzen. S tim (snad) problemy nemam, ale me proste vadi, ze je narovne ty mock testy behem vyvoje udrzovat a to ani nemluvim o tom, kolik moznych kombinaci je nutne prokryt v urcitych pripadech. Z toho prameni moje pochybnosti, zda ten cas venovany psani mock testu je pro me takovym prinosem.

  5. Honza Novotný Says:

    Já jsem se naopak přistihl, že v podstatě velmi málo programuji unit testy. Obvykle se jedná o testy integrační. Velmi často jsou zodpovědnosti tříd tak malé, že by ten test v podstatě nic moc neotestoval. Teprve složením více tříd dohromady vzniká nějak rozumě funkční celek. Typicky tedy dělám testy, které testují spolupráci dvou tří tříd. Mocky mi potom izolují tyto kooperující třídy od zbytku světa - a přestože refakturuji docela dost, nepřipadá mi, že by mi mocky nějak výrazně stěžovaly práci. Nicméně pravda je, že to s mocky taky extra nepřeháním ;).

    Zajímalo by mě Petře, co myslíš tím, že nepoužíváte TDD. Znamená to, že nedodržujete pravidlo "Test first" nebo to, že odladíte aplikaci a teprve v závěru píšete testy?

    Pokud to první, tak jsme na tom stejně. Zatím jsem nedospěl k tomu nejdřív psát testy. Obvykle nejdříve napíšu kód a pak test. Přičemž k odladění kódu právě používám test (testy). Defakto kód + test vzniká současně. Pokud bych psal test jako první, musel bych ho dost šíleně přepisovat, protože psaní vlastního kódu je u mě dost živelné. To co vypadá, že na začátku by mohla dělat jedna třída, v závěru dělají třeba třídy tři atd.

    Před dvěma, tři lety jsem psal testy až když byla aplikace celá hotová - sloužily mě jen k zafixování status quo. Což považuji ze současného pohldu za pěknou pitomost. Jednak jsem musel před manažery obhajovat čas strávený nad psaním testů (prostě to bylo "strašně vidět" - přitom žádná funkčnost už nepřibývala) a jednak jsem si neušetřil žádnou práci. Pokud se ale píšou testy současně, průběžně vzniká funkcionalita s testy a psát testy je i zábavnější. Navíc se tím děsně šetří čas, protože si člověk odladí většinu aplikace jen v prostředí IDE - navíc je daleko jednoduší testovat aplikaci po menších částech než jako celek s opakovanými deploymenty na server.

    Mám ten pocit (neověřený), že v současné době píšu výsledný kód i testy rychleji, než kdybych vyvíjel jen samotnou aplikaci s neustálými deploymenty + ručním ověřováním a laděním chování této aplikace. Samozřejmě významnou měrou k tomu přispívá i Spring - ten člověka jednak vede k lepšímu rozpadu funkčnosti a navíc má skělou podporu pro testování.

  6. Vitek Tajzich Says:

    Ahoj vsem, ja osobne jsem dospel o stavu TDD a opravdu pisu prvni testy. Pro me osobne to ma spoustu vyhod. Jednak kdyz mam pred sebou neco, co musim otestovat, tak v tu chvili si musim presne vedet jak se to ma chovat (vstup a co se mi vrati...), coz mi i pomaha s navrhem.
    Dalsi je i to, ze kod, ktery vznika pomoci TDD mi prijde kratsi a cistsi. Pokud postupujete stylem, ze si napisete test na jednu variantu a tu potom co nejjednodusim zpusobem naimplementujete a pote dopiste dalsi cast a zase co nejjednodussi implementace, tak finalni kod vznika mnohem rychleji a je celkove cistejsi.

    Mocky pouzivam hodne a s chuti. Konkretne pouzivam Mockito, pripadne PowerMockito. Ostatni jako EasyMock apod mi prijdou strasne ukecane.

    Duvod proc je pouzivam je jednoduchy. Musim otestovat nejmensi moznou jednotku a k tomu si potrebuji "odstrihnout" zbytek sveta, ale i nasimulovat jestli se ten kus kodu chova korektne kdyz se zbytek sveta chova nejakym zpusobem. Mocky tohle vsechno umoznuji.

    PowerMock je uzasna vec kdyz testujete kod, ktery pro svou praci pouziva knihovny tretich stran a tyto knihovny maji napr. metody final nebo staticke metody atd. PowerMock dokaze zmenit byte code v dobe natahovani classy a omockovat cokoli 😉