Programátoři a jejich manažeři občas řeší, jaká míra pokrytí unit testy je dostatečná. Můžeme zaslechnout, že cokoliv pod 60% nestojí za zmínku, občas zahlédneme, že to magické číslo je 95% a někteří fundamentalisté dokonce tvrdí, že cesta k nirváně vede jen a pouze přes stoprocentní pokrytí.
Dnes jsem si řekl, že je ideální počasí na to, vyzkoušet jestli se dá sta procent dosáhnout. Povedlo se mi to během dopoledne a nyní bych se chtěl podělit o své zkušenosti.
Pracuji na jednom menším projektu, který v podstatě jen zpracovává REST volání a přeposílá je pomocí SOAP na další systémy. Není to nic složitého, ale není to ani žádná trivialita. Prostě ideální kandidát na pokusy. I před tím, než jsem se rozhodl dosáhnout vytoužené mety, jsem se snažil unit testy dělat poctivě. Jen občas jsem něco neotestoval s tím, že to je příliš triviální na to, abych se patlal s testem. Většinou jsem ale svoji lenost ovládl. Proto mě překvapilo, že moje pokrytí testy bylo něco kolem 60%.
Začal jsem pátrat po příčinách a zjistil jsem, že nejsem tak svědomitý, jak jsem si myslel. Narazil jsem například na neotestovanout třídu, kterou jsem jen odněkud zkopíroval Fůůůůůj. Ale občas je to potřeba. Znáte to, potřebujete něco podobného co umí nějaká metoda ve Springu, jenom je to potřeba trochu ohnout, má to implementovat jiné rozhraní a navíc je to v tom Springu private. Tak to vezmete, zkopírujete a trochu přiohnete. Pokud se pokoušíte o stoprocentní pokrytí, musíte to i otestovat. Pokud se spokojíte s pokrytím nižším, je velice lákavé si říci, že to už je vlastně otestovaný kód, tudíž není potřeba testovat. Ještě jednou fůůůj. Ještě že se stydím jenom před děvčaty. Nakonec jsem byl rád, že jsem tu zkopírovanou metodu musel otestovat. Díky tomu jsem se nad ní pořádně zamyslel a zkrátil ji na polovinu.
Pak jsem ještě napsal pár testů na metody, které jsem nepovažoval za hodné testu. Potěšilo mě, že jsem narazil jenom na jednu metodu, kterou jsem evidentně netestoval, protože by to bylo moc práce. Díky své nové stoprocentní mantře jsem se donutil otestovat i ji. Nakonec to ani moc nebolelo, jenom jsem se musel ponořit do vnitřností Spring-WS abych zjistil jak nejlépe vytvořit instanci jednoho rozhraní (SoapMessage). Alespoň jsem se něco naučil. Pak už zbývaly jen zapeklitější věci.
Get a Set metody
Jedním ze zádrhelů byly get metody. Poté co jsem otestoval všechen kód, který něco dělá, zbyla mi jedna skupina metod, které prostě v žádném testu nebyly potřeba. Byly to get metody u Spring bean. Když totiž potřebuji pomocí Springu něco injektnout, automaticky vygeneruji set a get metody. A to i když používám autowiring přímo do polí. Set metody využívám právě v unit testech. Ukázalo se, že pokud není v testu set metoda použita, je v tom testu něco špatně. A to bez výjimky. Buď to, co nastavuji, není vůbec potřeba nebo netestuji funkcionalitu, která danou hodnotu využívá. Takže si prosím zapamatujte:
Pokud není otestována set metoda, je něco špatně.
Ale zpátky ke get metodám u Spring bean. Ty pro test potřeba nejsou. Ony totiž nejsou potřeba vůbec. Jenom jsem měl hrozný psychický problém s tím je smazat. Pořád jsem si říkal, že je tam nechám, co kdyby byly potřeba. Co kdyby někdo potřeboval vědět, jak je daná třída nakonfigurována atp. Ale pak mě stoprocentní mantra donutila je smazat a myslím, že si na ně nikdo nevzpomene. Jenom je to divné mít set bez getu. Kdybych ty get metody opravdu někdy potřeboval, třeba kvůli JMX, tak je prostě přidám a otestuji.
Equals
Dalším špekem je equals metoda. Pokud byste náhodou nevěděli jak vypadá, tak vám ukáži, to co mi normálně generuje Eclipse
public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof Person)) return false; Person other = (Person) obj; if (firstname == null) { if (other.firstname != null) return false; } else if (!firstname.equals(other.firstname)) return false; if (secondname == null) { if (other.secondname != null) return false; } else if (!secondname.equals(other.secondname)) return false; return true; }
A teď si představte jak otestovat všechny možnosti takového krasavce. Abyste dosáhli plného pokrytí, museli byste napsat cca devět testů. To vše kvůli vygenerovanému kódu. K tomu mě nikdo nedonutí. Zkusil jsem použít EqualsBuilder, ale moc jsem si nepomohl. I s jeho pomocí jsou tam asi čtyři kombinace, které je potřeba otestovat. Mumlaje si pod vousy „sto procent, sto procent“ jsem se zeptal pana Googla a ten mě navedl na EqualsTester. Problém vyřešen, jsem zas o krok blíže k vytouženému cíli.
Nemožná výjimka
Zbývala mi jen jedna úplně poslední past nastražená na mě pány ze Sunu:
private byte[] toByteArray(String data) { try { return data.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { LOG.error("Can not happen",e); throw new RuntimeException(e); } }
Abych dosáhl kompletního pokrytí, musím otestovat i catch blok. Ten je bohužel dosažitelný jen pokud vaše JVM nepodporuje UTF-8. Vzhledem k tomu, že by pak JVM nesplňovalo specifikaci není tato situace moc pravděpodobná. Bohužel, tím pádem ani otestování není moc snadné. Nakonec jsem musel kód změnit takto:
private static final Charset UTF8 = (Charset)Charset.availableCharsets().get("UTF-8"); private byte[] toByteArray(String data) { return data.getBytes(UTF8); }
Není to moc elegantní, ale alespoň se mohu utěšovat tím, že je to výkonově efektivnější. Pokud znáte lepší cestu, podělte se o ni, rád se něco přiučím.
A to je vše, dosáhl jsem stoprocentního pokrytí. Zde mi samozřejmě hrálo do karet, že jsem nemusel řešit připojení k databázi. To bych se musel uchýlit k mockování JDBC template nebo k mé oblíbené in-memory databázi ve spojení s JPA. To už by sice nebyl unit test, ale třeba by to nikdo nepoznal.
Takže mám stoprocentní pokrytí, co mi to dalo? Donutilo mě to otestovat věci, na které bych se pravděpodobně jinak vykašlal. U některých z nich jsem byl rád, že jsem je nakonec otestoval. Přivedlo mě to k lepšímu kódu. Potěšilo mě, že jsem nemusel psát žádné nesmyslně testy, jenom kvůli pokrytí. Toho jsem se ze začátku trochu bál. Samozřejmě nesmím usnout na vavřínech. To že mám kód zcela pokryt neznamená, že v něm není chyba. Těch tam naprosto jistě ještě hodně zůstalo. Nicméně mám z toho docela dobrý pocit, myslím si, že 100% je docela dobrá laťka. Na jednu stranu vám nedovolí vymýšlet si výmluvy, proč něco netestujete, na stranu druhou se ukázala jako dosažitelná. Proto se příště jako správný úderník pokusím pokrýt na 105%.