Python: Pozor na deepcopy

V práci jsme řešili takový nemilý problém, kdy v určitém stavu aplikace dokázala sežrat tolik paměti, až ji museli admini otočit. Ač to není nic dobrého, měli jsme štěstí. Zjistili jsme totiž, při jaké akci se to děje. Tudíž jsme si mohli z poloviny oddychnout…

Jednalo se o operaci, kde se prováděla obrovská kopie pomocí deepcopy. Instance nebyly malé, ale ani zase tak obrovské, aby se využití paměti po operaci navýšilo o pár řádů. Nedávno jsme přidávali zpětné reference a tak jsem se na ně zaměřil. I přesto, že jsme se jim se spojením deepcopy věnovali.

Překvapivě netrvalo dlouho nalézt problém. Měli jsme takovouto strukturu:

Container
 - Item (s referencí na container)
 - Item (opět s referencí na container)
…

Při kopírování celého containeru si deepcopy se zpětnou referencí poradí hravě. Pokud jste někdy přetěžovali magickou metodu __deepcopy__, jistě víte, že tam je parametr memo. Zde se předává slovník s objekty, které už byly zkopírované. Klíč je ID staré instance a hodnota je zkopírovaná instance. Tudíž když se má kopírovat znovu stejná instance, vezme se odsud. Programátor však tohle řešit nemusí, Python se postará sám.

Problém nastane, pokud se rozhodneme kopírovat itemy postupně.

for item in container:
    new_container.append(copy.deepcopy(item))

Vidíte to také? S každou položkou se zkopíruje i container. A nejen ten – container má referenci na položky, takže se zkopírují s každou položkou všechny položky! Nejenom, že se reference úplně rozjedou, ale navíc to při velkých strukturách sežere vše, co najde.

Možná řešení jsou dvě. Buď předávat <code>memo</code>, jenže to by musel řešit container a při kopírování pouze položek odjinud by vznikal problém znovu; nebo před kopírováním položky odmazat zpětnou referenci a po operaci ji vrátit. My zvolili druhou možnost.

Tím se problém téměř vyřešil. Bohužel se stále při kopírování mírně navyšovala podezřele paměť. Hledal jsem tedy dál a  po čase jsem našel příčinu – flyweight. To je pattern, kdy je v celém programu jen jedna instance jednoho objektu. Využíváme to s úspěchem u číselníků. Představte si to jako modely v Djangu a kdykoliv si vyžádáte nějaký záznam, nedostanete celý objekt, ale jen referenci na již existující. U naší velké aplikace flyweight hodně pomohl s optimalizací.

Každopádně jsem až po letech využívání zjistil, že deepcopy nečetl vůbec nic o patternech, natož nic o flyweight, a v klidu nám tyto instance kopíroval. Stačilo do flyweight třídy implementovat metodu __deepcopy__, aby vracela self, a nyní je vše v pořádku.

Deepcopy může hodně potrápit. Před použitím si zkuste v hlavě představit, co se stane. Detailně projděte, co kopírujete. Sice vás uživatelé nepoplácají po zádech, že myslíte na vše možné, ale lepší než ve stresu řešit provozní problémy. :-)

Hraní kostek v databázi

Jednou jsem řešil získání náhodného záznamu z databáze. Moc jsem se nad tím nezamýšlel – jednoduše jsem si vytáhl všechny záznamy, z kterých chci vybírat, a vybral náhodný index. Použil jsem to s databází MongoDB, kde zatím ani random není podporováno, takže mi vlastně ani jiná možnost nezbyla. Mohl jsem si takové naivní řešení dovolit z důvodu jistoty malé kolekce, která nebude růst. (Kód.)

V nedávné době jsem se dostal k zajímavému problému – jak něco takového udělat v tabulce, která bude obrovská a bude se i nadále zvětšovat? Řešení jsem nakonec nepotřeboval, ale nedalo mi to…

Získat si veškerá data do aplikace, z nich vybrat jeden záznam a zbytek zahodit může znamenat pád aplikace kvůli nedostatku paměti. Zkusil jsem tedy vzít funkci RAND a nějak sestavit dotaz. První co jsem měl bylo:

SELECT * FROM table ORDER BY RAND() LIMIT 1

Překvapilo mne však náročnost takového dotazu. Použil jsem tedy EXPLAIN a podíval se, co se děje. Samozřejmě – sortování celé tabulky bez indexu a vyhodnocením funkce RAND pro každý řádek. Sice je to lepší řešení, protože už mi nebude padat aplikace kvůli nedostatku paměti, ale efektivní to stále není. Navíc já chci jen jeden záznam – nepotřebuji seřadit celou tabulku, abych zahodil vše, krom prvního záznamu.

Začal jsem hledat na internetu, jestli je nějaká efektivní cesta, aby se používal index. Našel jsem řešení přes JOIN – zjistit si rozmezí IDček, vybrat jedno náhodné a to propojit s mojí tabulkou.

SELECT t1.* FROM table AS t1 JOIN (SELECT CEIL(RAND() * (SELECT MAX(id) FROM table)) AS rand_id) AS t2 ON t1.id = t2.rand_id

Efektivní, trochu nejasné na první pohled a hlavně nesmí být v tabulce díra. Musí existovat všechna IDčka. Tedy ne úplně vhodné řešení. S myšlenkou, že v části JOIN se funkce RAND vyhodnocuje pouze jednou, lze vymyslet další řešení, která problém s chybějícími záznamy vyřeší. Osobně mi to ale přijde takové řešení těžce čitelné. Bez popisu by mi chvíli trvalo zjistit, co takový dotaz dělá.

Zkusil jsem tedy pokračovat. MySQL a PostgreSQL mají OFFSET, tak mě napadlo využít ten.

SELECT * FROM table LIMIT (SELECT CEIL(RAND() * COUNT(*)) FROM table), 1

Bohužel takový dotaz nebude fungovat, subquery nelze v klauzuli LIMIT použít. Musíme tedy dotaz rozdělit na dvě části – nejprve vypočítat OFFSET a poté provést SELECT. Je možné tyto dvě operace provést jedním dotazem, otázkou však je, zda není lepší použít předchozí řešení s JOINem.

To mi však stále nestačilo. Hledáním jsem narazil na knihu SQL antipatterns. Jelikož je od The Pragmatic Programmers, obsahuje kapitolu o této problematice a další zajímavé kapitoly, přidal jsem si ji na wishlist a nedávno mi přišla domů. Z knihy jsem se už nic nového k hledání náhodných dat nedozvěděl; jsou tam podobné ukázky v jiném pořadí.

Pomohly mi ale jiné závěry z jiných kapitol. Například v kapitole o Spaghetti Query se dozvíte, že není dobré se snažit vytáhnout vše jedním monstrózním dotazem. Bude těžce čitelný pro vás, bude těžké ho udržovat a rozšiřovat a databáze může mít problémy ho správně zoptimalizovat. V kapitole o fulltextu se zase píše o použití správných nástrojů na správnou věc, nesnažit se za každou cenu použít databázi.

Proto kdybych dnes měl v aplikaci z velké tabulky tahat náhodná data, napsal bych dva dotazy.

Nenechte se zlákat deployováním Gitem

Vyzkoušel jsem si různé způsoby deployování webových aplikací a došel jsem k tomu, že poslední trendy nemusejí být zdaleka nejlepší volbou. Čímž posledním trendem myslím deployment Gitem.

Abych hned neurazil zastánce – sám využívám tento druh deploymentu například pro LunchtimeAnděl.cz. V nedávné době jsem místo ruční konfigurace použil POD, což je „Git push deploy for Node.js“, díky čemuž se nemusím o hooky starat sám. Jednoduše commitnu kód, pushnu a aplikace se restartuje. Spokojenost, žádné námitky.

Problém ale nastává, když se jedná o větší projekt. Například BOObook.cz – Pythoní aplikace se závislostmi nejen na PyPI, ale i na debianí balíky, ve velkém množství. Vedle toho CoffeeScript s Closure Library. Elasticsearch, memcache, nginx. PostgreSQL a migrace databáze. Cron soubor, init script. A další.

Z počátku, když spoustu věcí nebylo doprogramováno (tedy daleko před spuštěním) byl deployment Gitem velice jednoduchý úkol. Fungovalo to parádně. Postupem času se věci komplikovali a jelikož to nebyly neřešitelné problémy, s radostí jsem rozšiřoval hooky, které se mi o všechno postaraly.

Když jsem si rozbíhal druhé testovací prostředí, zjistil jsem ale, že Git přestává vyhovovat. Bylo to příliš komplikované a náchylné na chyby. Navíc jsem neměl nic, co mi dokáže čistý stroj připravit k provozu. To se mi nelíbilo a tak jsem nahradil Git hooky za Fabric. To mi umožnilo přesně to, co jsem potřeboval – přípravu serveru, deployment, automatické tagování, syncování databází a spoustu dalšího. Něco, co už z podstaty s hooky nelze udělat.

Několik měsíců jsem byl spokojen, nyní však zjišťuji, že už ani Fabric není dostačující. Respektive je, ale některé věci jsou řešeny zbytečně komplikovaně. Například řešení závislostí či instalace souborů do systému. Proto další krok bude přímo debianí balíček, který se mi postará o poslední nedostatky.

Tím se mi vyřeší i spoustu menších nedostatků. Třeba rozhodně nechci kompilovat JavaScript či styly až na serveru, proto musím mít minifikované verze už v repozitáři. Nebo se mi nelíbí nutnost aktualizovat Git submoduly na serveru kvůli fontům, které nechci kopírovat do svého repozitáře, což zabírá zbytečně čas.

Pak budu spokojen stejně jako s Git hooky pro LunchtimeAnděl.cz.

Co z toho plyne? Je dobré si vyzkoušet různé možnosti. Pokaždé se hodí totiž něco jiného. Nenechte se zlákat deployováním Gitem, pokud váš projekt potřebuje něco jiného.

Využívat více Makefile, nebo ne?

Už dlouho mám v hlavě otázku „proč se tak málo využívá Makefile?“ Nebo ho snad využívám k něčemu, na co se nehodí? Osobně si u každého projektu Makefile vytvořím a takový základ (jak u čeho) jsou tyto příkazy:

make install-dev
make install-git-hooks
make compile
make compile-css
make compile-js
make watch
make test
make run
make install
make deploy

A jsem s tím velice spokojen. Chápu, že je Makefile definován jako nástroj pro překlad zdrojových souborů do binárních. Proč ale zůstávat jen u toho? Dá se na Makefile nahlížet také jako na nástroj volající bash scripty.

Mohlo by se namítnout, proč využívat Makefile na volání scriptů? Důvodů by se našlo několik. Například bych scripty musel napsat, zde řeším už jen co se má dělat. Napovídání cílů tabulátorem. Samozřejmě i závislosti – jednoduše zařídím, že před testováním se musí nejprve provést kompilace atp.

Každopádně jsem zkusil zapřemýšlet, zda opravdu potřebuju jakýkoliv script. Zda nestačí utility dané technologie. Jelikož jsem programátor, tvorem líným, ukázalo se, že potřebuji. Jsem líný se učit nazpaměť příkazy, kór když se musí volat pokaždé s jinými parametry dle projektu.

cp git-hooks/* .git/hooks/
apt-get install [what is in readme]
pip install -Ur requirements.txt
npm install
recess --compress
coffee --watch -cb
nosetests tests
mocha tests --watch
python setup.py install
python setup.py sdist upload

Takže jsem došel k závěru, že je opravdu skvělé mít Makefile a neřešit, co musím udělat, abych si spustil testy. A když nemám potřebné balíky, nemuset dolovat, které musím nainstalovat, ale jednoduše zavolat make install-dev.

Zdá se, že s tímto názorem nejsem rozhodně sám. Není nás ale mnoho. Co používáte vy a proč?

Znej svoje IDE

V práci jsme narazili na téma o chytrosti IDE. Možná vás to překvapí, ale nebyla řeč o tom, jak jsou hloupí, ale o tom, jak moc chytrý jsou a že to je spíš na škodu.

Konkrétně toto téma vyvolalo našeptávání pro Python, kdy PyLint upozorňuje na metody, které ve skutečnosti nemusí být metody (PyLint R0201). Prý je to už moc a žádné takové upozornění by podobný nástroj neměl dělat. Protože přeci programátor ví, co dělá. Pak neprogramuje programátor, ale nějaký nástroj.

Já si osobně myslím, že to není špatná věc. Jednou, možná dvakrát, se mi už stala situace, kdy po refaktoringu mi zůstala metoda, kterou jsem opravdu chtěl dát nakonec jen jako funkci, a PyLint mi to připomněl. Není však nutnost takové našeptávače mít.

Každopádně, co chci říct – neustále si zlepšujeme naše nástroje tak, abychom manuální stereotypní práci dělali co nejméně. Což je plně v pořádku. Důležité je však dobře znát, co ty nástroje vlastně dělají. Všem bych doporučil začít programovat s nejhloupějším editorem a s konzolí. Postupně pak proces vývoje posouvat k těm chytřejším nástrojům.

Má to spoustu výhod. Při takovém postupu víme, co se děje. Víme, co si můžeme dovolit. Dokážeme si poradit i u jiného počítače, serveru či webovém IDE. A taky si dokážeme udělat vlastní vývojové prostředí na míru, kde zautomatizujeme kde co.

Poté se nestane, že si IDE dělá co chce a programátor vlastně jen kouká a občas někam klikne.

P.S.: Možná se může zdát, že tu mixuji dvě věci – IDE s PyLintem nemá skoro nic společného. Ale není to tak. PyLint se sám v konzoli nespustí, kdežto IDE ho sám spustí na pozadí a ihned kód podtrhne. Navíc to lze aplikovat i na jiné věci, například vyhledávání v souborech, debugger, refaktorovací nástroje, SCM, našeptávání…

Django signály

Nemám v úmyslu zde podrobně popisovat, jak signály fungují. Od toho tu je dokumentace. Chci pouze upozornit, že existují, lze je šikovně využít, ale taky si s nimi lze nadrobit na problémy.

K čemu signály jsou? Jednoduše k decoupling. Zrušení závislostí v kódu. Představme si to na ukázce, třeba typu e-shopu:

E-shop se skládá ze spoustu menších aplikací a každá dělá jednu svou věc. Jedna z těchto aplikací jistě bude napojení na platební bránu. Po zaplacení platby ale určitě nebude stačit pouze upravit v databázi stav platby, ale bude chtít také změnit stav objednávky, poslat zákazníkovi informaci o stavu, odeslat informace na sklad, … a spoustu dalších věcí záležící dle konkrétního e-shopu.

Klasicky by v appce payment musel být kód, který bude importovat ostatní appky e-shopu a volat jejich metody. Co se právě stalo? Appka payment není univerzální. Nejde ji dát do odděleného repozitáře a využít jinde. Je navždy spojena s konkrétním kódem e-shopu.

Jak by to vypadalo se signály? V payment appce se definuje signál, který se bude vyvolávat při změně platby. Ostatní appky budou na tento signál pouze vyčkávat. Tím bude platební modul přenositelný i do jiných aplikací. Sice je pravda, že nyní je situace opačná – ostatní appky jsou závislé. Ale… tak je to přeci správně, ne? :-)

Pro inspiraci – na BOObook.cz o produktech, krom základních atributů jako název či cena, žádná jiná appka nic neví. Nemusím tak nikde natvrdo psát, aby se po zaplacení zákazníkovi knihy zapsaly do knihovničky či poslaly mailem. Jednoduše se v appce ebook čeká na spuštění signálu o zaplacenosti a poté se zde zařídí vše potřebné.

Tím jsem docílil toho, že mohu vzít kód e-shopu krom produktů, napsat novou definici produktů třeba na zubní pasty a vše mi bude fungovat. Aniž bych musel cokoliv jiného měnit. Skvělé!

Podobně s fakturačním modulem. Nikde nevolám invoice.create_invoice(...), místo toho fakturační modul čeká na ten správný signál. Tím mohu mít dva fakturační moduly (jeden velmi základní a jeden komplexní s napojením do účetnictví) a zaměnit je dle libosti bez úpravy kódu. Dle toho, o který si zákazník řekne.

Než se ale vrhnete do zkoušení – signály nejsou řešením pro všechno. Je potřeba se zamyslet, zda využití signálu bude opravdu užitkem. Aspoň určitě se budete důkladně zamýšlet, až začnete hledat první bugy v těchto úsecích kódu. Není to zábava. :-) Nadměrné použití signálů také ztíží čitelnost kódu, protože už nebude jasné, kdy se co volá. Takže opatrně s nimi!

Nedostatek v tvorbě HTML5 offline webů

Chtěl jsem dlouho omrknout lákavou část HTML5 specifikace, a to tvoření webové aplikace i pro offline režim. Jelikož je podpora na dobré cestě (Chrome 4.0, Firefox 3.5, IE 10), rozhodl jsem se místo nějaké minutkové stránky udělat rovnou něco užitečného – například offline stránku pro BOObook.cz, kde by byla omezená nabídka s možností vložit do košíku a synchronizací ihned po obnově spojení. K tomu ještě možnost zobrazit si poslední známý stav košíku a wishlistu. Jen takové srandičky.

Bohužel jsem ale narazil na nepříjemnou drobnost, díky které už nemám takovou chuť si s offline verzí webové aplikace tolik hrát. Nejprve ale zběžně vysvětlím, jak to celé funguje – do tagu html se vloží atribut manifest s odkazem na soubor manifestu. Takový soubor může vypadat například takto:

CACHE MANIFEST
# v1 2011-08-14
# This is another comment
index.html
cache.html
style.css
image1.png

# Use from network if available
NETWORK:
network.html

# Fallback content
FALLBACK:
/ fallback.html

Který obsahuje tři části (nezáleží na pořadí a každá sekce se může vyskytnout vícekrát nebo vůbec). První v ukázce je CACHE, která určuje, jaké soubory se mají cachovat. Tato sekce se nemusí explicitně uvádět, je to výchozí sekce (v ukázce se jedná o index.html, cache.html, …). Další je NETWORK, kde se sděluje, které soubory se mají načítat z internetu. Vhodné například pro skriptíky sledující návštěvnost. A pak tu je FALLBACK sekce oznamující jakou jinou stránku zobrazit, pokud požadovaná stránka v cache není.

Není to nic složitého. Jen si dejte pozor: zástupná znak hvězdičku nelze použít v sekci CACHE a ve FALLBACK sekci je první parametr prefix, pro které pravidlo platí; tedy lomítko znamená všechny stránky. Pro podrobnější popis hodím 303 na kapitolu Let's take this offline z Dive Into HTML5.

V odkazovaném článku se jako ukázková aplikace popisuje Wikipedia s touto funkcionalitou: jakákoliv navštívená stránka se automaticky dostane do cache a pokud požadovaná stránka není dostupná, zobrazí se speciální stránka. To je pro „statický“ obsah s užitečnými informacemi dobré řešení.

Horší to je s dynamickým webem, kde není žádoucí cachovat každou stránku. Může se jednat o sociální síť, e-shop či jinou dynamickou aplikaci, kde se každá stránka zobrazuje s měnícími se stavy. Také by stránky mohli zbytečně zabírat spoustu místa. Proto cachování každé stránky v tomto případě postrádá smysl nebo by vedlo jen ke zmatení uživatele. Zde by dobrým řešením mohlo být jen sdělit fallback a jinak vše tahat čerstvé z internetu.

Něco takového bohužel nejde udělat, protože každá stránka obsahující manifest je automaticky cachována aniž by se to explicitně uvedlo v manifestu. Nejde to potlačit ani s hlavičkou Cache-Control: no-cache, no-store. To je dost nepříjemné a tato drobnost mi přijde jako velký nedostatek.

Hned by mohlo napadnout, že se manifest může dát jen na jednu určitou stránky a na ostatní nikoliv. Pravda, ale stále jedna stránka bude cachována a hlavně(!) uživatel musí danou stránku navštívit. Pokud uživatel tak neudělá, prohlížeč se nedozví o manifestu a offline verze jakoby neexistovala.

Řešení ale neleží daleko, jen se musí trochu hackovat – vytvořit offline stránku, která jako jediná bude mít odkaz na manifest a nebude na offline stránku nikde odkaz. Tuto stránku potom přibalit ke každé stránce v tagu iframe. Tím se zajistí, že online stránky se do cache nedostanou, a přitom se o manifestu prohlížeč dozví z jakékoliv stránky. (Já říkal, že to bude hack. ;))

K lepšímu řešení jsem se zatím nedobral ani po pátráním po internetu. Snad jednou bude možné hack zahodit a udělat to systémovější cestou.

    Co mi vadí na Pythonu

    Občas mě kamarád poprosí o pomoc se zapeklitým problémem, většinou v PHP. Přijdu, pomůžu vypátrat a nezapomenu zmínit, jak je PHP nelogické, nekonzistentní, plné WTF momentů a jak jsem rád, že jsem tento svět opustil. Dokonce jsem kamaráda nahlodal natolik, že si začal o Pythonu číst. Zajímalo ho ale taky, když mám tolik výtek k PHP, kolik a jaké výtky mám k Pythonu? To mě trochu zarazilo, nikdy jsem o tom nepřemýšlel. A na nic se mi nedařilo přijít. Nevzdal jsem se, vytvořil si prázdný texťák a postupem si zapisoval co se naskytlo. Výsledkem je následující seznam…
    • Výchozí parametr ve funkci/metodě se vyhodnotí jednou při kompilaci, nikoliv při volání. Tedy pak vzniká tento problém:
    >>> def foo(l=[]):
    ...     l.append(42)
    ...     print l
    ... 
    >>> foo()
    [42]
    >>> foo()
    [42, 42]
    • Datový typ bool je ve skutečnosti také int. Při podmínkování na datový typ se nesmí zapomenout nejprve zjišťovat, zda se nejedná o bool a teprve potom o int. Tím to nekončí, jsou tu další zajímavé vedlejší účinky:
    >>> isinstance(True, int)
    True
    >>> True + True
    2
    >>> {1: 'one', True: 'true'}
    {1: 'true'}
    • Líbí se mi, že si lze pomoct závorkami zalomit text bez nutnosti použití ošklivých zpětných lomítek. Bohužel to s sebou přináší možnost vznik chyb, které se velmi těžko hledají…
    >>> t = ('a', 'b' 'c')
    >>> len(t)
    2
    >>> # Tohle je ale fajn.
    >>> (
    ...     'some very long '
    ...     'sentence...'
    ... )
    'some very long sentence...'
    • Také se mi líbí, že tuple lze zapsat bez nutnosti závorek (tím pak lze zapsat například for k, v in d.items()). Jen když se někde nechtěně objeví čárka nebo se naopak na ni zapomene…
    >>> 2,
    (2,)
    >>> (2)
    2
    • Je fajn, že se mohu vybrat svobodně mezi tabulátory a mezerami. Co už ale fajn není je, že mix je povolen. Když se pak otevře soubor na chvíli v jiném editoru a nevšimnu si špatného nastavení, Python mi nezahlásí SyntaxError.
    • Chápu důvod, proč se musí explicitně k metodám psát self. Ale, opravdu by to nešlo bez toho? Nejednou jsem zapomněl self napsat.
    • Relativní importy jsou skvělá věc, ale limituje to v použití názvů. Aneb naštve když mě konečně napadne skvělý název pro soubor s mojí super třídou a o chvilku později zjistím, že mi to koliduje s nějakou knihovnou, kterou používám. To lze tedy potlačit direktivou from __future__ import absolute_import nebo použitím Pythonu 3, takže už to není takový problém.
    • str a unicode. Kolik chyb tohoto problému jsem viděl! Kéž by všichni psali tak, aby byl všude unicode a převode na str jen naprosto v nutném případě. Naštěstí tohle řeší Python 3.
    • Naneštěstí použití Pythonu 3 není tak jednoduché. Nekompatibilita je nekompatibilita a tak ne všechny používané knihovny zmigrovaly. Což je u větších projektů dost limitující (vždycky se najde knihovna, kvůli které přejít nelze).

    A to je vše, víc mě nenapadá. Celkově mám Python velmi rád. Možná proto, že je to rebel; kamarád mi řekl, že Python mění jeho svět programování. Jinde jsou totiž podobné zápisy vyhodnoceny jako syntax error. Aneb díky Pythonu se mohu soustředit více na problém, který řeším, a nepřemýšlet, jak něco napsat.

    Tipy na testování webových aplikací

    Testováním webových aplikací se zabývám už poměrně dlouho a přijde mi, že integrační testy v podobě Selenia jsou mnohem důležitější, než jednotkové testy JavaScriptu nebo backendu. V různých knihách a článcích se píše o tom, že pokud není praxe, ať se testuje dle toho, co danému programátorovi přijde vhodné. Tím se získá praxe a najde se ten cit pro rozhodování jaké napsat testy přednostně a čím se nemá smysl zabývat.

    Také jsem tak začínal a cit si vybudoval. I když, stále se mám co učit. Dnes už ale vím, že to není zrovna nejlepší poučka. Minimálně co se týče webových aplikací. Tam už vím, že nejlepší je sáhnout prvně po Seleniu. Důved je velmi prostý – jednotkové testy, jak JavaScriptu, tak backendu (ať už je to jakýkoliv jazyk), otestují jen nějakou tu jednotku. Řekne to programátorovi „hele, tenhle kus kódu je správný. Jestli ale náhodou šablona očekává něco jiného a nezobrazí tlačítko, na kterém to je závislé… to už nevím. Ověř si sám.“ nebo tak nějak.

    Tohle však testy bohužel neřeknou. Odpoví jen stručné „OK“. Jakmile tuto odpověď uvidí programátor, jde radostně nasadit na betu, oznámí uživatelům že mohou testovat a ejhle. Ona tam je chyba v šabloně (nebo v čemkoliv jiném, co už jednotkové testy nepodchytí) a programátor musí po víkendu zjišťovat, cože to v pátek dělal a cože to musí opravit.

    Neříkám však, že ostatní testy nejsou dobré. Taky mi odhalili spoustu chyb, Selenium však jednoznačně vede.

    To je tedy tip číslo jedna – u webovek se zaměřit více na Selenium.

    V naší aplikaci v práci mi hodně vadilo, že tam máme spoustu chyb v JavaScriptu. Webmasteři se u nás ještě do nedávna střídali jeden za druhým, každý si to dělal po svém a nikoho nezajímalo, co tam je a co tam bude. Dnešní situace je už dobrá a neustále se zlepšuje, bylo mi to ale málo a napadlo mě do našich Selenium testů přidat kontrolu JavaScriptových chyb. Do stránky jsem přidal kus JS:

    <script type="text/javascript">
    window.jsErrors = [];
    window. function(errorMessage) {
      window.jsErrors[window.jsErrors.length] = errorMessage;
    }
    </script>

    a v Selenium (po každém testu, v tearDown) přečetl obsah proměnné jsErrors. Spustil to a… nalézalo to jednu chybu za druhou. Tady className neexistuje, tajhle null nemá atribut getElementsByTagName apod. Vlastně ještě dnes to nalézá takové chyby díky tipu číslo tři, o tom ale až za chvíli.

    Úprava mě stála asi hodinku se vším všudy a dostal jsem mocnou zbraň proti bugům v JavaScriptu. Stačí aby se na něco opomnělo a testy zařvou. Druhý tip je myslím jasný – přidat do Selenia podporu na hledání JavaScriptových chyb.

    Je tedy pravda, že takhle selžou testy, které ve skutečnosti prošly. „Jen“ nastala nějaká drobnost v JavaScriptu. Já na to odpovím: nezájem. Je tam chyba? Je. Opravit.

    Říkal jsem, že nám tu testy stále oznamují různé chyby v JavaScriptu díky třetímu tipu. Tento tip zní: fuzzy testování.

    Zmíněná aplikace je velmi obrovská a testy se začaly psát teprve někdy před rokem a půl. Stále máme spoustu nepokrytého místa, které se zpětně těžce vyplňuje (nelze se vybodnout na všechny známé bugy a feature requesty, na měsíc se zavřít ve sklepě a dopsat všechny testy). To mě pochopitelně trápilo a vyřešil jsem tento problém alespoň fuzzy testováním. Během chvilky jsem napsal primitivní algoritmus na náhodné proklíkání webové aplikace.

    Neočekával jsem, že to něco pořádně najde, ale světe div se… ono to našlo hned chybu na dvacátý náhodný klik. Klik, který by neudělal žádný programátor (protože ví, kam klikat nemá), ale ani tester či uživatel. Tedy, věřím, že to někdo už udělal, jen se nenamáhal s reportem. Fuzzy testování jsem zatím nechal v primitivní podobě jak jsem to poprvé napsal a stále to nachází různé fatální chyby, které měly skončit jen chybovou hláškou. Někdo by mohl konstatovat, že to je drobnost, nic kritického. Uživatel má však opačný názor.

    Minimálně v našem případě to hodně pomáhá s najitím chyb v JavaScriptu různě v aplikaci, kam se Selenium testy sami od sebe jen tak nepodívají.

    Zbývá poslední tip. Selenium je takové neohrabané. Hodně mi vadil styl použití. Sice používám webdriver (Selenium 2), což je lepší, než předchozí verze, ale stále to není ono. Takže jsem napsal wrapper, který práci se Seleniem ulehčí. Kód jsem vystavil volně na GitHubu a ještě mám nějaké plány na zlepšení než to převedu do stable verze. Každopádně už nyní to používají dvě aplikace. Více zde: https://github.com/horejsek/python-webdriverwrapper. (Jedná se o Python.)

    Pokud bych dnes začínal nový projekt, s těmito testy bych začal.

    Programování bez komentářů

    Hodně vídám v kódu komentáře. To by nebylo to nejhorší, jsou situace, kdy jsou komentáře opravdu důležité, ale většinou narážím na situace, kdy je tomu opačně. Takové komentáře mám čím dál více nerad.

    Přitom by stačilo málo – než se napíše komentář, stačilo by se zamyslet, zda je opravdu důležitý. Jako nejběžnější zbytečné komentáře jsou totiž takové komentáře, které popisují to, co už popisuje sám kód.

    Pokud však kód nemluví sám za sebe (a zdá se, že je komentář nutný), pak je pravděpodobně samotný kód napsán špatně. Stačí se podívat zda nepomůže lepší pojmenování nebo rozdělení funkcionality na menší dílky atp. Prostě refaktorovat.

    Ve výsledku by měl být komentář nutný opravdu jen ve výjimečných situacích.

    Je mi jasné, že psát bez komentářů není jednoduché, chce to praxi. Jenže bez zkoušení se praxe nezíská. Hezky to popisuje Steve Yegge:

    In the old days, seeing too much code at once quite frankly exceeded my complexity threshold, and when I had to work with it I'd typically try to rewrite it or at least comment it heavily. Today, however, I just slog through it without complaining (much). When I have a specific goal in mind and a complicated piece of code to write, I spend my time making it happen rather than telling myself stories about it.

    S tím se vlastně pojí čistý kód. Doporučuju si přečíst knížku Clean Code nebo jsem o tom také psal na Zdrojáku v krátkém seriálu, kde se dozvíte jak psát kód, který mluví sám za sebe.

    Naivně jsem měl za to, že komentáře budou ubývat a ubývat, ale opak je pravdou. Proto tento blogpost. Zahrajme si hru programujeme bez komentářů!