Unit testy a globální stav

Unit testy a globální stav – jak to jde dohromady? Vůbec! Za žádných okolností! Takové bylo pro mě hlavní poselství přednášky Miška Heveryho z Googlu, kterou měl v březnu 2011 na ČVUT. Pokud chcete psát dobrý (dobře testovatelný) kód, tak se opravdu použití globálního stavu vyvarujte. Jak ale identifikovat přítomnost globálního stavu v kódu?

Typickým a snadno odhalitelným globálním stavem je globální proměnná (static v C#), často jako součást implementace Singletonu. Globální proměnnou by v ideálním případě neměl náš kód používat. Nikdy.
Btw. „naším kódem“ (zde i dále) myslím především jádro naší aplikace – business logiku, doménovou logiku, …

Jednoduchá a univerzální poučka zní, že pokud váš unit test vyžaduje úklid (teardown), pak testovaný kód s největší pravděpodobností pracuje s globálním stavem. Potřebujete např. po skončení testu smazat nějaké soubory z disku nebo vymazat nějaké hodnoty z databáze? Pak se může jednat o integrační test a pokud si to uvědomujete, tak to může být ok. Pokud se má ale jednat o skutečný unit test, tak je něco v nepořádku. Unit test by měl totiž testovat jen jednu jednotku a to v dokonalé izolaci – a pokud nejsme schopni používaný globální stav odizolovat, pak je náš testovaný kód shnilý a měli bychom s tím něco dělat.
Máme tedy abstrahovat přístup k databázi nebo souborovému systému? V případě relační databáze jsme často díky ORM již odstíněni a slušné ORM pak není problém odizolovat a jako backend použít místo skutečné databáze jen kolekci v paměti. A u přístupu k souborovému systému není zas takový problém si vytvořit odstiňující vrstvu. Ale chápu, že to někdo může považovat za zbytečné vrstvení – pragmatickým přístupem pak může být pro takovou třídu nepsat unit test, ale jen integrační test.

Posledním, ne tak zřejmým, zdrojem globálního stavu, o kterém chci psát, je zjištění aktuálního času – vlastně to spadá do prvního zmiňované kategorie (použití globální proměnné, viz např. DateTime.UtcNow v C#), ale člověk si to tak neuvědomuje. Udělali tedy snad inženýři v Microsoftu chybu, že nám dali k dispozici aktuální čas přes globální proměnnou? Ne, jejich kód je z hlediska testovatelnosti ok. Pokud to dává smysl, klidně zavádějte globální proměnné – ale pokud chcete psát testovatelný kód, tak globální proměnné nepoužívejte napřímo. To znamená, že je vhodné z hlediska testovatelnosti odstínit údaj o aktuálním času za nějaké rozhraní ICurrentDateTimeProvider.

Jak jsem nadhodil na Twitteru, na první pohled se to může zdát jako over-engineered řešení – a dostalo se mi několika zajímavých reakcí.

Aleš Roubíček navrhuje udělat pro každou metodu, která potřebuje aktuální čas, další overload, který má další parametr typu DateTime. To je zajímavé řešení, ale nelíbí se mi, že si zaprasíme veřejné API oné třídy. Ano, metoda s parametrem navíc by mohla být internal a mohli bychom použít InternalsVisibleTo, ale…;-)
Dobrý protiargument proti Alešově řešení nadnesl Dan Kolman – co když nějaká jiná metoda volaná během testu používá onu metodu bez parametru (a tedy používající DateTime.UtcNow) ? Pak jsme v koncích…

S další reakcí přišel Filip Kinský, který navrhuje toto řešení. To je docela fajn, protože nevoláme napřímo standardní DateTime.Now, jejíž hodnotu nemáme možnost změnit. Nyní můžeme změnit delegáta, který obstarává získání aktuálního času. Problém je ale v tom, že tato závislost není povinná (nikdo nás nenutí nastavit property CurrentDateTime) a třída tak v deklaraci konstruktoru neinformuje o této závislosti. A to je z mého hlediska problém, protože na nepovinnou závislost nevěřím – toto sousloví vnímám jako oxymóron 🙂

Vzásadě jsme se ale shodli, že odstínit přístup k aktuálnímu času (coby globálnímu stavu), je z hlediska testovatelnosti dobré.

Shrnutí na závěr je takové, že byste se měli ve svém kódu vyvarovat použití globálního stavu napřímo – z hlediska testovatelnosti (která jde ruku v ruce s dobrým návrhem obecně) je vhodné mít přístup ke globálnímu stavu odstíněn, abychom ho mohli kdykoliv nahradit.
Ještě bych dodal, že každá třída by měla mít všechny své závislosti dostat přes parametry konstruktoru (volání konstruktoru se nemůžeme vyhnout). Zároveň ale pamatujte na Law of Demeter a předávejte do třídy je ty závislosti, které třída přímo potřebuje.

11 thoughts on “Unit testy a globální stav

  1. Dobrá poznámka. Sami autoři Moles ale říkají, že preferovaná cesta je zrefaktorovat kód, aby se daly použít Stubs (takže zavést rozhraní) a Moles použít jen v nejnutněších případech, kdy není zbytí…

    To se mi líbí

  2. Ad Moles. Nejde jenom o to nějakým způsobem docílit testovatelnosti, jde i o to, že kód s hard-coded závislostmi na globálním stavu

    a) je méně přehledný (viz oxymoron nepovinné závislosti – pokud třída interně používá nějaký singleton pro připojení k databázi, jako její uživatel budu muset nastudovat dokumentaci a před prvním použitím správně nastavit onen singleton. Pokud by taková třída vyžadovala připojení k db jako parametr konstruktoru, dokonce kompilátor sám nám ohlídá správné použití — v podstatě nepůjde zmiňovanou třídu špatně použít)

    b) méně flexibilní (co když budeme chtít místo aktuálního času v UTC použít aktuální místní čas, na tohle by Moles doufám žádný rozumný vývojář nepoužil)

    Nejdůležitější je vybrat si postupy a technologie adekvátní řešenému problému. V jednorázovém pár set řádkovém programu by používání DIP bylo opravdu šíleností.

    To se mi líbí

  3. Netvrdím, že má člověk psát špatně testovatelný kód a pak to mockovat přes Moles, to vůbec ne.
    A přestože v pár projektech mám ICurrentDateTimeProvider, zrovna na DateTime.Now mi přijde čistší použít ty Moles – zavádět kvůli takové prkotině jako DateTime.Now rozhraní a třídu mi přijde jako kanón na vrabce.
    Navíc spousta lidí je zvyklá DateTime.Now používat a donutit některé programátory v mém týmu, aby na ten ICurrentDateTimeProvider nezapomínali, je těžší problém, než se zdá.

    To se mi líbí

  4. Ad internal, to jsem nikde ani nenaznačoval 🙂

    Ad „nějaká jiná metoda volaná během testu používá onu metodu bez parametru“ – pak je to integrační test a já mluvil v kontextu unit testování.

    Netvrdím, že se mi koncept IClock nelíbí, ale ne vždy je nutné vytvářet abstrakci na úrovni typu, někdy si vystačíme s pouhým přetěžováním.

    To se mi líbí

  5. Ad „nějaká jiná metoda volaná během testu používá onu metodu bez parametru“ – pak je to integrační test a já mluvil v kontextu unit testování.
    Jak naznačil Dan, tak unit testování nemusí znamenat, že testujeme každou metodu zvlášť. Resp. považovat za jednotku testování metodu mi nepřijde šťastné – víc sexy mi přijde testovat features 🙂
    A považovat test, ve kterém testovaná metoda volá jinou metodu téže třídy, za integrační test, to považuji za TDD purismum 🙂

    To se mi líbí

  6. ještě k tomu času: čas je zvláštní v tom, že on je opravdu globální. Určitě chceme, aby v aplikace mohlo být více instancí různých časů?

    Podle mě spíše řešíme, že náš čas není reálný čas a nebo používáme dva globalní časy (třeba reálný čas PC a business čas). V logu pak bude, že v čase reálném se stala událost nějakého jiného času – v tomto případě tedy jde vlastně dvě globální property a třeba i jiné třídy. Ten bussiness čas vlastně není čas, to jsou nějaké jiné tiky.

    To znamená, že bych se nebála někde používat globální DateTime.Now, protože ten je opravdu globální na celé planetě a tam kde potřebuji business čas bych asi použila nějakého Providera.

    Jinak s těmi globálními proměnnými souhlasím, ale je třeba najít hranici, kdy je to přínos a kdy naopak. DateTIme.Now je jen taková zvláštní konstanta. (A co DateTime.MinValue?)
    Zkrátka pokud je nějaká věc opravdu globální, tak bych ji globální ponechala.

    Jen nevím, jestli najdu ještě něco jiného, co je tak globální jako je čas.

    To se mi líbí

  7. DateTime.Now je všechno jen ne konstanta 😉

    Základní předpokladem unit testu je, že je vždy opakovatelný a SUT vrací vždy stejné výsledky pro dané parametry. V případě, že předáváme, nebo podstrkáváme nestále se měnící čas, musí to test ovlivnit a tudíž neůe vracet stále stejné výsledky. Pak takový test není z definice unit testem 😉 A už vůbec se na něj nemůžeme spolehnout.

    To se mi líbí

  8. ..psala jsem něco jako konstanta…z určitého úhlu je víc konstantní než zrychlení 9.81…ale to je jedno…
    To co píšeš o testech je pravda, ale to hlavní co jsem chtěla říci je, že rozlišuju reálný čas, a business čas. Obvykle v aplikaci používáme oba časy, protože mají jiný význam, a (pokud) testujeme, tak jen to co závisí na tom businessovém. A tam preferuju „IClock“ namísto přetěžováni.

    To se mi líbí

  9. Vlaďko, s možnou přítomností dvou časů (skutečný a business) souhlasím.
    Aktuálně to mám tak, že business čas je vždy vztažen k nějaké entitě, takže se tenhle business čas předává spolu s entitou v parametrech metody, příp. business čas je součástí entity.
    Určitě ale existuje úloha taková, že bude mít smysl existence ISystemClock a IBusinessClock vedle sebe.

    To se mi líbí

Napsat komentář

Tento web používá Akismet na redukci spamu. Zjistěte více o tom, jak jsou data z komentářů zpracovávána.