čtvrtek 9. prosince 2010

Maven content check plugin - JARy pod kontrolou

V HP i GoodData jsme řešili problém s obsahem našich WARu, EARu a dalších distribučních balíčků, ve kterých se nám poměrně neřízeným procesem objevovaly nové JARy, což při použití tranzitivních závislostí v Mavenu není jev zas až tolik vzácný. To byl problém ze dvou důvodů.

  • problémy s některýmy OSS licencemi. Pokud do vašeho produktu zatáhnete nějakou knihovnu s virální licencí může to znamenat, že vlastní zdrojový kód musíte open sourcovat pod tou samou licencí. Jiné licence mají zase problém s tím, že se na ně váže IP, především patenty. Pokud použijete licenci, která má určitý výklad týkající se patentů, může se stát, že se tím automatický vzdáváte vámi držených patentů souvisejících s danou knihovnou.
  • problémy s tím, že někdo se rozhodně použít knihovnu XYZ a vy přitom ve zbytku produktu používáte knihovny jinou.

Tyto problémy se dají vyřešit poměrně jednoduchým způsobem. Máte autoritativní seznam souborů, který se může v daném archivu vyskytovat. Tento seznam kontroluje Maven Content Check Plugin a v případě, že archív obsahuje něco jiného, build selže s detailním výstupem, které konkrétní soubory nesedí. Konfigurace pluginu a formát autoritativního seznamu najdete v dokumentaci.

Plugin toho zatim moc neumí, za zmíňku stojí

  • dokáže ignorovat JARy, které produkujete vy a to podle obsahu jejich manifestu
  • dokáže reportovat i reverzní případ tedy že nějaké chybí

V dalších verzích pluginů se plánuje iniciální generování autoritativního seznamu podle obsahu archívu. Detekce typu licence pro daný JAR a její vypsání.

neděle 5. prosince 2010

Něco málo k testům

Byl jsem požádán, abych se podělil o zkušenosti s knihovnou JUnit 4 a zkušenosti obecně s psaním testů za použití této knihovny. Původně jsem to chtěl odbít jenom jednoduchou větou, že na jejím používání nic není, ale pak mě napadlo, že to není vůbec o té knihovně, ale o návycích, které jsem si vypěstoval při psaní testů. V tomto článku se kromě JUnitu zmíním o tom na co si dát pozor, jak psát testy a nebo třeba o skvělé knihovně Mockito, se kterou se testy píší snadněji. Doufam, že si každý v tomto článku najde to svoje.

JUnit

Předesílám, že jsem nikdy nepracoval s knihovnou TestNG a proto pokud vám v JUnit něco chybí, nevylučují že vám to tato knihovna neposkytne.

Když Kent Back psal JUnit verze čtyři, byly anotace už nějaký ten pátek součástí Javy. Zatímco v trojkové verzi JUnit jste museli dědit od třídy TestCase a vaše testovací metody musely obsahovat ve jménu slovo test, už si nejsem jistý, jestli na něj nemusely začínat, ve verzi čtyři stačilo použít anotaci @Test a označit metody, ve kterých byly vlastní testy.

Ve verzi čtyři přihodil JUnit další anotace a konstrukty, které jsem začal používat, ale k dokonalosti jim něco chybí. Anotace @BeforeClass a @AfterClass slouží k přípravě a úklidu před a po testech dané třídy. Určitě chválihodné, až na to, že mohou být použité pouze na statických metodách, což jejich použitelnost trochu degraduje. Rozumný důvod proč musí být statické jsem nenašel.

Chválihodné bylo zavedení čitelnější metody pro ověřování v podobě assertThat, v podstatě jiný typ assertu už ani nepoužívám.

import static org.hamcrest.CoreMatchers.is; 
Cart cart = new Cart();
cart.put("something"); 
assertThat("The method size returned wrong count of items in the cart.", cart.size(), is(1)); 

Problém s tímhle konstruktem je v tom, že jako druhý respektive třetí argument, pokud použijeme fail zprávu, bere org.hamcrest.Matcher a to je úplně jiná knihovna. To by nebylo až tak špatné, ba naopak, jenom kdyby se Kent Beck nerozhodl tenhle problém jiné knihovny vyřešit tím nejhorším možným způsobem. Prostě vzal část Hamcrest a zapekl jí včetně originálního balení do JUnitu.

To má samozřejmě za následek, že pokud chcete použít novější verzi Hamcrest tak máte smůlu. Novější verzi chcete většinou použít ve chvíli kdy si píšete vlastní Matcher a chcete, aby měl lepší fail message.

V jedné z pokročilejších verzí čtyřky se objevil i třída Assume s metodou assumeTrue(boolean). Přiznám se, že z počátku jsem z ní byl nadšený, ale postupem času mě to nadšení opustilo. Měla by se použít, že chcete před začátkem testu ověřit nějaký předpoklad bez jeho naplnění nemá smysl test pouštět. Problém s tímhle udělátkem je v tom, že jaksi nerozlišíte jestli test proběhl a nebo byl zaignorován. Takže to snižuje vypovídající hodnotu výsledku.

Obecné rady

Pište asserty se zprávou, která zasadí selhání testu do kontextu. Pokud takový test selže pomůže vám to pochopit jenom z výsledků testu co se kde pokazilo a nemusíte pak pracně dohledávat, který assert nedopadl a proč. To je zvláště užitečné pokud máte v metodě několik assertů. Zároveň to napomáhá i k čitelnosti a dokumentaci testu, kdy vidíme z jakého důvodu tam daný assert je.

assertThat("The method size returned wrong count of items in the cart.", cart.size(), is(1)); 

Používejte jména testovacích metod tak, aby vyjadřovala co testujete.

@Test 
public void testCartSize()....

Nekombinujte test jedné funkčnosti se všemi možnými asserty v jedné testovací metodě. Rozdělte asserty do testovacích metod a ty pojmenujte daným případem. Zlepšíte tím čitelnost a přehlednost testu.

@Test 
public void testCartSize() {
  Cart cart = new Cart(); 
  cart.put(new Item());  
  assertThat("The method size returned wrong count of items in the cart.", cart.size(), is(1)); 
  Cart cart = new Cart(); 
  assertThat("The method size returned wrong count of items in the new cart.", cart.size(), is(1));
} 

By mělo být

@Test 
public void testCartSize() {
  Cart cart = new Cart(); 
  cart.put(new Item());  
  assertThat("The method size returned wrong count of items in the cart.", cart.size(), is(1));
}

@Test 
public void testCartSizeOnNewInstance() {
  Cart cart = new Cart(); 
  assertThat("The method size returned wrong count of items for a new car  instance", cart.size(), is(1));
} 

Obecně čím jednoduší je kód testu a kratší tělo testovací metody tím lépe.

Vyhněte se testům, které spoléhají na jistou formu načasování. Nedávno jsem musel opravit test, který ověřoval, že hodnoty budou po určité době automatický odstraněné z cache.

 cache.put("Foo");
 assertTrue(cache.contains("Foo"));
 Thread.sleep(1000);
 assertFalse(cache.contains("Foo"));

Ten test někdy selhal a někdy prostě prošel a to ačkoliv se čekalo relativně dlouho. Problém byl totiž v tom prvním assertu. Pokud se totiž mezi provedením řádku 1 a 2 pustilo čistící vlákno cache, tak ten test jednoduše selhal. Odhlédněme od faktu, že v tomto případě, když už chceme otestovat nějaký předpoklad, tak by dávalo smysl použít asssumeTrue.

Především měla být konfigurace toho testu, kde se samozřejmě testoval i reversní případ s tím, že nějaké hodnoty se vrací z cache, nastavená tak, aby se čistící vlákno úplně vyřadilo ze hry. Pouze pro ten konkrétní případ se měl udělat setup kdy čistící vlákno bude aktivní, ale nebude se tam dělat ten první assert.

Obecné pravidlo, kterého je potřeba se držet zní: pokud není test spolehlivý, je lepší ho nemít. Doporučuji nemilosrdně přepsat testy, které vám někdy projdou a jindy ne, a vy nedokážete zjistit proč. V testech neexistuje něco jako 99.9 úspěch. Je pouze a jenom úspěch a nebo ne. Jiná interpretace typu ten test občas selže, to je ok, a to že jsem jí zažil, je jenom lhaní si do kapsy.

Důrazně bych doporučoval dodržet, aby testovací metody začínaly prefixem test, protože potom je pěkně přehledné, co je skutečná testovací metoda a co jsou nějaké helper metody. Zároveň je dobré pojmenovat test k dané třídě jménem třídy plus sufixem Test. Nic neudělá větší nepořádek, než když každý vývojář používá vlastní konvence.

V této souvislosti mi docela vadí, že IDE nekontroluje tuhle, řekl bych zaběhlou, konvenci. Mám tím na mysli, zapomenutou anotaci @Test na testovací metodě. Kolikrát jsem se radoval nad prošlými testy, abych pak nadával na to, že část se vůbec nepustila, protože jsem tam tu anotaci zapomněl vložit.

Jestli mají lidé odpor k psaní testu, tak je to většinou proto, že to je pro ně složité. Platí zlatá poučka, pokud se vám pro něco těžko píše test, pak v tom kódu něco smrdí. Pokud jde o složitý setup mohu jenom doporučit skvělou knihovnu Mockito.

Mockito

Mockito umí dělat mocky a stuby, tedy kromě toho, že něco vypadá jako kachna, tak to umí i jako kachna kvákat a umí to řící kolikrát to kváklo.

Item item = Mock.mock(Item.class);
Mockito.when(item.getPrice()).thenReturn(10); 
Cart cart = new Cart();
cart.put(item); 
assertThat(cart.getTotalSum(), is(10));
Mockito.verify(item).getPrice(); 

Jak je vidět práce s Mockitem je opravdu velice jednoduchá. Připravíme se stub Item, u kterého chceme, aby nám při volání metody pro získání ceny vrátil hodnotu deset.V tomto případě poslední verify, tedy že se na stubu něco stalo, není potřeba, a uvádím to zde pouze pro ukázku. Za použití Mockita nebo jiných podobných frameworku klesá komplexita psaní testů a to řádově. Já osobně jsem opravdu z Mockita nadšený, odpadlo mi totiž v testech tuna balastu, který byl potřeba pro úspěšný setup.