Zobrazují se příspěvky se štítkemTestování. Zobrazit všechny příspěvky
Zobrazují se příspěvky se štítkemTestování. Zobrazit všechny příspěvky

26. března 2012

Acceptance test driven development

Pro většinu z nás jsou unit testy naprostou samozřejmostí a pokud budu mluvit sám za sebe, tak si to bez unit testů už nedovedu představit. Nejvíce to vidím, když opravuji starý kód, jak jsem nervózní, abych jednou úpravou neudělal další chyby.

Osobně unit testy (nebo i integrační testy, myšleno testy nad databází) píšu po napsaní produkčního kódu. Již během psaní produkčního kódu si dělám poznámky, jaké možné cestičky je potřeba ověřit a podle toho potom píši testy. Testy tedy hlavně beru jako mého pomocníka (=pomocníka programátora), abych byl schopen dodat práci co v nejlepší kvalitě a byl co nejlépe připravený na budoucí úpravy a opravy. Z pohledu samotného vývoje je asi tento pohled v pořádku, ale pokud se na proces vývoje ve firmě podívám z pohledu celé firmy, tak už tam vidím nějaké mezery.

Proces vývoje většinou začíná u konzultantů, kteří tvoří zadání, kteří definují akceptační kritéria, kteří nesou zodpovědnost za to, že zákazníkovi se dodá to, co opravdu chtěl a navíc v požadované kvalitě. Od začátku jsou tedy zřejmé požadavky, které je nutné splnit a tyto požadavky by se měly co nejvíce přenést do samotného vývoje. Je tedy žádoucí, aby programátoři se během vývoje a testování zaměřili právě na tyto důležité požadavky. Často programátor testuje věci dle svého uvážení, kde on sám vidí možné slabiny, ale to je často v rozporu s tím, co musí testovat konzultant/tester.

Právě při těchto úvahách jsem hledal možné řešení, jak co nejvíce propojit obě části vývoje, jak najít společnou řeč. Nejblíže mojí představě je Acceptance test driven development a konkrétně projekt Thucydides, který celému přístupu dává jasné obrysy.

Advanced practices of test-driven development can lead to Acceptance Test-driven development (ATDD) where the criteria specified by the customer are automated into acceptance tests, which then drive the traditional unit test-driven development (UTDD) process. This process ensures the customer has an automated mechanism to decide whether the software meets their requirements. With ATDD, the development team now has a specific target to satisfy, the acceptance tests, which keeps them continuously focused on what the customer really wants from that user story.
Místo vysvětlování ATDD a Thucydides se doporučuji podívat na prezentaci pod článkem, kde je vše pěkně ukázané. Pro další informace pak ještě doporučuji článek z JavaWorldu - Acceptance test driven development for web applications.
Ještě bych zmínil knihovnu jBehave, která je představitelem přístupu zvaném Behaviour-Driven Development (BDD), který je velice podobný ATDD.

Rádi bychom šli v celém návrhu ještě dále a využili JIRU jako nástroj pro psaní a evidenci kritérií resp. stories. JIRA je pro většinu týmu centrálním bodem, kde se evidují požadavky na vývoj a je to i nástroj, který umí používat konzultanti. Tak proč neupravit JIRU i pro zadávání akceptačních kritérií v určitém formátu, ze kterých by se následně generovala kostra/šablona pro psaní testů v Groovy nebo jUnit? 

Nápadů a myšlenek je hodně, uvidíme v jaké podobě (a zda vůbec) se nám to podaří realizovat v praxi. Pokud máte někdo s uvedenými přístupy nějaké zkušenosti, budu rád když je uvedete v komentářích k článku.

1. ledna 2011

Knihovny pro testování: MockFtpServer a GreenMail

Minulý rok jsem toho napsal zatím nejméně a rád bych, aby to ten letošní rok bylo lepší. Proto píšu hned první den - jak se říká, jak na nový rok, tak po celý rok :).

Na posledním projektu jsem měl potřebu si ověřit správnou komunikaci s FTP serverem a posílání/přijímaní mailů, tedy komunikace se SMTP serverem a IMAP resp. POP3 serverem.

Pro komunikaci s FTP serverem jsem použil knihovnu Apache Commons Net. Nejdříve jsem chvíli přemýšlel, zda mi nebude stačit podpora ve standardní Javě (pro moje potřeby naprosto dostačující), ale nakonec jsem si vybral Commons Net. Rád a často používám knihovny s rodiny Commons a i zde jsem rád, že jsem tuto knihovnu vybral.
Pro testování komunikace s FTP serverem jsem našel výbornou knihovnu MockFtpServer. Dokáže opravdu věrohodně simulovat FTP server, zejména možnosti nastavení práv souborového systému jsou super.


Pro odesílání mailů používám podporu ve Springu - JavaMailSenderImpl. Kromě odesílání jsem potřeboval maily i přes IMAP stahovat, vytvářet složky na mail serveru a s maily různě manipulovat. Čekal jsem, že najdu nějakou vhodnou knihovnu, ale nakonec jsem skončil u standardní Javy, která nabízí JavaMail API. Některé věci jsou možná až moc nízkoúrovňové, ale nic lepšího jsem nenašel.

Pro testování mailů jsem využil knihovnu greenmail. Jako jedna z mála knihoven, které jsem našel pro testování, umí kromě ověřování SMTP i IMAP/POP3 protokol.

Pokud někdo potřebuje testovat jen SMTP, tak pro to by asi úplně stačila knihovna SubEtha SMTP.

15. října 2010

jUnit a hamcrest - na pořadí v pom.xml záleží

Pro testování používám jUnit a mojí oblíbenou knihovnu hamcrest. Hamcrest je již nějakou dobu součástí jUnit - takže člověk si může vybrat používat pouze jUnit včetně hamcrestu (core) a nebo jUnit a k tomu si dotáhnout celý hamcrest (all).

Já jsem zvolil druhou variantu a dlouho jsem řešil ten problém, že se mi špatně vypisovala chyba assertu. Vždy když nějaká podmínka nebyla splněna, tak se mi zobrazila tato chyba:

java.lang.NoSuchMethodError: org.hamcrest.Matcher.describeMismatch(Ljava/lang/Object;Lorg/hamcrest/DescriptionV

Zjistil jsem, že řešení je jednoduché - je jen potřeba mít správné pořadí závislostí v pom.xml. Nejdříve mít referenci na hamcrest-all a pak teprve na jUnit.

<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.4</version>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>xmlunit</groupId>
<artifactId>xmlunit</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>2.5.6</version>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.8.2</version>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>

Odkaz na relevantní blogy:

1. dubna 2010

Plošné vypnutí povinného @Autowired

Při testování naší agendiové aplikace jsem se již několikrát dostal do stavu, že složitost a propletenost celé aplikace mi neumožňovala napsat rozumně testy. Zejména jsem měl problém vůbec vše potřebné nakonfigurovat, aby se všechny závislosti správně nastavily.

Po nějakém čase mě napadlo, že by možná bylo vhodné pro účely testování vypnout autowiring jako povinný - tedy z @Autowired(required = true), což je implicitní chování, změnit na @Autowired(required = false). Tím získám tu velkou výhodu, že pro testy budu muset inicializovat pouze ty závislosti, které opravdu potřebuji. Tím budu mít méně konfigurace pro jednotlivé testy, což se rovná menším nárokům na údržbu testů, zejména konfigurace testů.

Zatím jsem nezjistil žádnou velkou nevýhodu uvedeného řešení, kromě toho, že je nutné v produkčním kódu plošně používat autowiring pomocí anotací. Trochu mě zaráží, že jsem o této myšlence ještě nikdy neslyšel.

A jak na to? Autowiring pomocí anotace @Autowired se děje pomocí bean post-procesoru AutowiredAnnotationBeanPostProcessor.

Mám dvě možnosti jak plošně změnit chování autowiringu:

  • v konfiguraci bean post-procesoru změním pomocí metody setRequiredParameterValue implicitní hodnotu parametru required.
  • vytvořím si novou implementaci bean post-procesoru, kde přepíšu metodu determineRequiredStatus.

/**
* Vlastni konfigurace {@link AutowiredAnnotationBeanPostProcessor} pro testy.
*
* <p>Jedina zmena je v tom, ze defaultne je anotace {@link Autowired} nastavena na {code required=false},
* tedy pokud se pres auto-wiring nenajde reference, tak to nebude vadit.
*
* <p>Test Context framework pouziva standardne svuj loader {@link AbstractGenericContextLoader}, ktery
* ovsem interne automaticky registruje vsechny mozne bean-post procesory,
* viz {@link AnnotationConfigUtils#registerAnnotationConfigProcessors(BeanDefinitionRegistry)}.
* Proto, aby chodil autowiring s required=false, pak je nutne:
* <ul>
* <li>pouzivat vlastni loader {@link AbstractDaisyTestContextLoader} pri spousteni testu
* <li>dat si pozor na konfiguraci {@link AutowiredAnnotationBeanPostProcessor} v ramci testovaciho Spring kontextu.
* </ul>
*
* @author <a href="mailto:petr.juza@marbes.cz">Petr Juza</a>
*/
public class AutowiredAnnotationBeanPostProcessorForTests extends AutowiredAnnotationBeanPostProcessor {

/**
* Vraci vzdy true, tedy neni nutne pres anotace {@link Autowired} mit dostupne vsechny reference.
*
* @param annotation the Autowired annotation
* @return vzdy true
*/
@Override
protected boolean determineRequiredStatus(Annotation annotation) {
return false;
}
}

Jak je v JavaDocu uvedeno, tak je potřeba si dát pozor na to, že Spring TestContext framework využívá svůj ContextLoader, který automaticky registruje AutowiredAnnotationBeanPostProcessor. Proto je potřeba mít svůj ContextLoader a podstrčit vlastní implementaci bean post-procesoru.

Díky tomuto řešení a díky tomu, že jsem objevil atribut primary u definice beanu, tak postupně měním svůj názor na autowiring (myslím pomocí anotací). Dříve jsem byl celkem konzervativní a byl jsem pro používání setterů, které je potřeba v XML nastavit. Důvod? Vše je jasné, nikde žádná magie, lehce mohu sledovat závislosti.
Pomocí anotací je obecně potřeba méně kódu a z pohledu testování mi to může přinést spoustu zajímavých výhod, jako např. tu zmíněnou v tomto článku.

7. února 2010

Testování webových služeb

Aplikace řadu funkcí a dat publikuje přes webové služby. Je to rozhraní naší aplikace, na které se většinou pojí aplikace třetích stran, a proto je žádoucí mít aspoň nějakou jistotu, že nám rozhraní přes webové služby funguje.

Webové služby jsou generovány dynamicky pomocí Apache CXF (pozn.: s tímto přístupem se neztotožňuji) a není výjimkou, že při změně verze CXF se změní i výsledné WSDL. Nebo stačí přidat/změnit/ubrat atributy ve třídách, které jsou publikovány přes webové služby a hned máme jiné WSDL. Proto považuji za velmi důležité vytvořit testy, které mi budou hlídat generované WSDL a upozorní mě, když dojde ke změně.

Na testování webových služeb existuje parádní nástroj SoapUI, který ovšem potřebuje běžící server resp. webové služby. Mým cílem bylo napsat jUnit testy webových služeb bez potřeby spouštění celého webového serveru, a proto jsem SoapUI zavrhl.

Snažím se psát testy pouze tehdy, když sám sebe přesvědčím, že čas vynaložený na testy nebude zbytečné psaní kódu bez většího užitku. V tomto případě jsem došel k názoru, že bude rozumné psát testy, které:

  • budou ověřovat správnou funkcionalitu publikované služby. Dle mého názoru není až tak potřeba simulovat posílání XML zpráv, protože veškeré mapování XML <-> Java <-> XML za nás provádí CXF a ten by již měl být otestovaný. Ze stejného důvodu nevidím moc velký přínos v psaní testů na JAX-WS mapování. V konečném důsledku se tedy jedná o testy nad normální "serviskou" bez ohledu na to, že je navíc zpřístupněna přes webovou službu.

  • mi zaručí konzistenci generovaných WSDL. Naše aplikace se u zákazníků postupně aktualizuje, aktualizuje se CXF v naší aplikaci, mění se třídy a je nutné zaručit, že aplikace třetích stran se stále budou pojit na to samé webové rozhraní jako v předchozích verzích naší aplikace.

Testy s CXF

Apache CXF nabízí, dle mého názoru slabou a špatně zdokumentovanou, podporu pro psaní testů. Nejzajímavější je třída TestUtilities. Abych mohl tuto třídu začít využívat, tak musím nejdříve inicializovat Bus.

Ověření konzistence WSDL

Pro ověření generovaných WSDL jsem zvolil následující postup:
  1. vygeneruji "referenční" WSDL do souboru a uložím
  2. napíši test, který mi bude porovnávat dynamicky generované WSDL s WSDL uloženým v souboru.
Není úplně šťastné porovnávat WSDL přes equals, ale raději použít XMLUnit.

Pro psaní testů webových služeb jsem vytvořil následujícího předka (pozn.: CxfJsr181HandlerMapping je naše třída pro inicializaci CXF a automatickou registraci webových služeb):

package cz.marbes.daisy.tests;

import cz.marbes.daisy.sysmodules.cxf.CxfJsr181HandlerMapping;
import org.apache.commons.io.IOUtils;
import org.apache.cxf.test.TestUtilities;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.ElementNameAndAttributeQualifier;
import org.custommonkey.xmlunit.XMLUnit;
import org.junit.Assert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.NotTransactional;
import org.springframework.test.context.ContextConfiguration;
import org.w3c.dom.Document;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;


/**
* Abstraktni trida (predek) pro testy webovych sluzeb, kde:
* <ul>
* <li>potrebuji ukladat/cist data z DB
* <li>jsou potreba transakce nad DB
* <li>potrebuji kontrolovat konzistenci webovych sluzeb (metoda {@link #porovnaniCxfWsdl(String, String)}).
* </ul>
*
* <p>Pokud dany test nevyzaduje transakci, tak pouzijte anotaci {@link NotTransactional}.
*
* @author <a href="mailto:petr.juza@marbes.cz">Petr Juza</a>
*/
@ContextConfiguration(locations = {"classpath:/applicationContext-ws.xml", "classpath:/applicationContext-sdb.xml"})
public class AbstractTransactionalDaisyWsTest extends AbstractTransactionalDaisyDaoTest {

private TestUtilities testUtilities;

@Autowired
private CxfJsr181HandlerMapping cxfHandlerMapping;


/**
* Vraci referenci na {@link TestUtilities}, pomocne tridy pro testovani webovych sluzeb z CXF.
*
* @return reference na TestUtilities
*/
protected final TestUtilities getTestUtilities() {
if (testUtilities == null) {
testUtilities = new TestUtilities(getClass());
testUtilities.addDefaultNamespaces();
testUtilities.setBus(cxfHandlerMapping.getBus());
}

return testUtilities;
}


/**
* Porovnava WSDL ze souboru a WSDL generovane za behu z CXF.
*
* <p>Metoda porovnava, zda jsou si WSDL podobne. Dve XML jsou si podobne, pokud maji
* stejne elementy, ale nezalezi na poradi elementu. Identicke XML musi byt stejne elementy ve stejnem poradi.
*
* @param serviceAddress Adresa sluzby, napr. {@code /webservices/digest/zuk_v1_1_2}
* @param wsdlFile Nazev souboru, kde je ulozene WSDL
* @throws Exception v pripade jakekoliv chyby
* @see #generovaniCxfWsdlDoSouboru(String, String)
*/
protected final void porovnaniCxfWsdl(String serviceAddress, String wsdlFile) throws Exception {
//wsdl ze souboru
String wsdlFromFile = IOUtils.toString(getClass().getResourceAsStream(wsdlFile));
Document origDocWsdl = XMLUnit.buildControlDocument(wsdlFromFile);

//wsdl z CXF
Document docWsdl = getTestUtilities().getWSDLDocument(getTestUtilities().getServerForAddress(serviceAddress));

Diff xmlDiff = new Diff(origDocWsdl, docWsdl);
xmlDiff.overrideElementQualifier(new ElementNameAndAttributeQualifier());

Assert.assertTrue("Porovnani WSDL ze souboru a z CXF, zda jsou podobne.", xmlDiff.similar());
}


/**
* Generuje WSDL z CXF do souboru.
* Tato metoda je vhodna pro generovani WSDL pro porovnavani s aktualne vygenerovanym WSDL za behu.
*
* <p>Metoda generuje pouze jedno WSDL. Nekdy se stava, ze hlavni WSDL importuje dalsi WSDL - to je tim,
* ze vsechny namespaces nejsou stejne. Nejcastejsi problem je ten, ze implementace webove sluzby ma jiny
* namespace nez rozhrani webove sluzby.
*
* @param serviceAddress Adresa sluzby, napr. {@code /webservices/digest/zuk_v1_1_2}
* @param wsdlFile Nazev souboru, do ktereho se ulozeni vygenerovane WSDL
* @throws Exception v pripade jakekoliv chyby
* @see #porovnaniCxfWsdl(String, String)
*/
protected final void generovaniCxfWsdlDoSouboru(String serviceAddress, String wsdlFile) throws Exception {
File exportWsdlFile = new File(wsdlFile);

//wsdl z CXF
Document wsdl = getTestUtilities().getWSDLDocument(getTestUtilities().getServerForAddress(serviceAddress));

TransformerFactory tFactory = TransformerFactory.newInstance();
Transformer transformer = tFactory.newTransformer();

//wsdl ulozim do souboru
DOMSource source = new DOMSource(wsdl);
StreamResult result = new StreamResult(exportWsdlFile);
transformer.transform(source, result);
}

}


Napsání výsledného testu je pak rutinní záležitost:

/**
* Test konzistence webove sluzby {@link WSAaZUK_v1_1_2} pomoci porovnani ulozeneho WSDL v souboru
* a nove generovaneho WSDL z CXF.
*/
@Test
@NotTransactional
public void testKonzistenceWsdlPresSoubor() throws Exception {
porovnaniCxfWsdl(SERVICE_ADDRESS, WSDL_FILE);
}

31. prosince 2009

Největší problémy při zavádění testování

Mám teď možnost zavádět testování do již existujícího projektu. Projekt je to celkem velký - přes 10 vývojářů, přes 100 tabulek, přes 50 tisíc tříd, několik let vývoje. Technologicky je to postavené nad Springem, Hibernate a spoustou dalších knihoven. O zavedení testování se již pár lidí přede mnou snažilo, ale vždy bez úspěchu, tak jsem zvědavý, jak teď dopadnu já. Hodně důležité je, že testování chce nyní i vedení, což je moc důležitý předpoklad - psaní a údržba testů stojí spoustu úsilí a času a pokud není podpora ze shora, tak se to musí pak "pytlíkovat" za zády a to není moc dobré.

Mám dojem, že většina vývojářů by testy psala celkem ráda, ale vždy diskuze na toto téma skončí u toho, že aplikace je to velice složitá, a že je problém vůbec nějak nastavit počáteční data pro testy.

Ještě je brzy na to, abych psal jak to celé dopadlo, to ještě bude nějaký čas trvat. Dnes bych chtěl psát o největších problémech, které jsem zatím musel řešit, abych vůbec mohl napsat nějaké testy:

  • "špagety kód" - určitě největší problém. Aplikace se vyvíjí již řadu let a zejména ten starý kód není nic moc. Když to přeženu, tak vše souvisí se vším a pak je problém inicializovat pro testy jen ty části aplikace, které potřebuji. Musel jsem refaktorovat, psát stub objekty, abych se dokázal vymezit. Tady není možné praštit do stolu a říci, že zítra se celá aplikace zrefaktoruje a bude to ok. Je to postupný proces. Tento problém se netýká jen samotného kódu, ale také konfigurace aplikace. Musel jsem jí rozdělit do částí tak, abych mohl např. inicializovat jen připojení k databázi nebo webové služby.

  • propojení s dalšími aplikacemi - aplikace závisí na spoustě dalších dílčích aplikací a proto je nezbytné se umět vymezit, nejlépe pomocí nové implementace konektorů pro testy (stub vs. mock objekty).

  • špatná dokumentace - to neplatí jen pro testy, ale obecně. Abych mohl co nejlépe a nejjednodušeji používat nebo implementovat rozhraní, tak potřebuji kvalitní dokumentaci (JavaDoc), jinak si pak musím vše dohledávat v kódu sám.

  • používání auto-wiringu - Springovský auto-wiring se používá pro konfiguraci celé aplikace, takže na první pohled nejsou vůbec jasné vazby mezi třídami, komponentami. Zde jsem musel hodně zapojit IDEU (zejména výborný nástroj na analýzu závislostí v kódu), abych se dopátral všech možných závislostí. Držel bych se doporučení autorů Springu a auto-wiring bych používal jen pro testy.

  • generované ID beanů - pokud v konfiguraci beanů neuvedeme atribut id nebo name, tak to neznamená, že daný bean identifikátor nemá, ale že se automaticky generuje, standardně podle jména třídy (rozhraní BeanNameGenerator). Takže když pak změním jméno třídy, tak se mi změní i identifikátor beanu. Je proto určitě lepší vždy definovat identifikátor, ideálně přes atribut id.

  • transakční propagace REQUIRES_NEW - testy nemají mít žádný vedlejší efekt, žádná uložená data v databázi po testech. Sem tam je v aplikaci použita propagace REQUIRES_NEW, což znamená, že v případě existující transakce se aktuální transakce pozastaví (suspenduje), spustí se nová transakce, která se pak kamitne a pak se znovu aktivuje původní transakce. Toto nemám ověřené, ale vidím zde možný problém pro testy, kdy by se vlastně měly všechny transakce na konci rollbacknout, ale co tyto vnořené transakce, které již jsou po komitu? REQUIRES_NEW se používá minimálně, tak možná to nebude potřeba ani testovat a když bude potřeba, tak zkusím následující možnosti - upravit transakčního managera nebo refaktorovat produkční kód.

Opět se mi potvrdila moje dřívější zkušenost, že čím kvalitnější produkční kód bude, tím lépe se mi budou psát testy. V tomto také vidím jeden z největších přínosů testování.

28. října 2009

Testování s Mockitem

Pokud to někdo myslí s testováním vážně, tak se asi bez mockování neobejde. Já jsem dříve používal jMock a musím říct, že mě to celkem na nějaký čas od mockování odradilo - samotný zápis mi přišel velice upovídaný, měl jsem problémy s refactoringem a hlavně to bylo celé hodně náročné na údržbu. Zhodnotil jsem to tehdy, že je to moc práce bez velkého přínosu. Ještě bych měl možná podotknout, že jsem psal testy vždy až po produkčním kódu.

Na nedávném setkání s vývojáři v Ostravě mi byla doporučena knihovna Mockito. Sice jsem s ní ještě nic nedělal, ale už mě chytla - kód je hezky čitelný (stylem mi to připomíná hamcrest), bezproblémový refactoring, není nutné psát metody pomocí řetězců a celé mi to prostě přijde takové intuitivní. Jinak o důvodech vzniku Mockita je možné si přečíst na stránkach autorova blogu.

Pokud se vám Mockito líbit nebude, tak zkuste něco z následujících knihoven (na které mám uloženy odkazy): jMockit, EasyMock, jMock a nebo rMock (pokud máte s těmito knihovnami nějaké zkušenosti, tak se rád přiučím).

Nakonec ještě posílám odkaz na zajímavý článek "Mocks Aren't Stubs" od Martina Fowlera.

27. září 2009

Vytváření testovacích dat

Každý, kdo píše testy, tak řeší problém s tím, jak nainicalizovat strukturu svých (doménových) objektů, aby mohl otestovat určitou funkcionalitu. Způsobů řešení je více.

"ruční" inicializace pomocí Javy

Pokud test potřebuje nějaké objekty, tak si je v rámci samotného testu prostě vytvoříme resp. přes IoC napojíme, stejně jako když vytváříme produkční kód.

Nevýhodou tohoto přístupu je, že ve výsledku vzniká spoustu duplicitního kódu, protože každý si vytváří své objekty. A naopak pokud se objekty vytvářejí centrálně, tak pak je většinou ten problém, že chybí možnost si pro určité testy nastavit určité atributy jinak než implicitně.

externí popis dat

Požadovaná data nadefinuji pomocí vhodného frameworku, tak abych oddělil vlastní testovací kód a testovací data. Ideálním představitelem pro databázové systémy je DbUnit, ale vlastně se jedná o jakýkoliv systém, který mi umožňuje externě definovat data, např. pomocí XML.

Výhod v tomto případě vidím několik - testy obsahují pouze kód k ověření funkčnosti a již nemusí řešit "omáčku" okolo, vytváření podobných a často se opakujících testů bude rychlejší a nakonec také testovací data a samotné testy nemusí vytvářet jedna osoba.

Na druhou stranu vím, že tímto způsobem nelze moc dobře vytvářet nějaké větší grafy objektů a také údržba aktuálnosti dat dá celkem zabrat, zejména když se pustíme do refactoringu.

testovací databáze

Pro účely testování bude k dispozici testovací databáze, která bude obsahovat nějaký konzistentní stav a k tomuto stavu budou psány testy.

Kromě náročnosti na údržbu dat zde vidím problém v případě, kdy nad touto testovací databází bude pracovat více vývojářů, kteří si budou navzájem zamykat data (např. při krokování programu). Také je jen otázka času, kdy někdo omylem komitne změny v testech. Řešením by zde bylo, kdyby každý vývojář měl svojí testovací databázi.

automatické vytváření grafu objektů

Při vytváření testovacích dat nám jde většinou o několik málo atributů, které ověřujeme a ostatní atributy mohou být zcela libovolné. To je základní myšlenka našeho jednoduchého řešení, které se nám stará o centrální vytváření grafu objektů, aniž bychom museli sami tyto objekty vytvářet pro každý test.

Postup je asi následující: každý objekt, který potřebujeme vytvářet během testů, bude mít odpovídající třídu "testData", která se bude starat o jeho inicializaci (nejen jednoduchých atributů, ale i o referencí na další objekty). Pro psaní těchto "testData" tříd jsme napsali dvě podpůrné třídy AbstractReadTestData a AbstractCreateTestData, takže stačí implementovat jen jednu resp. dvě metody ve vlastní "testData" třídě. Kdykoliv pak během testu potřebuji nějaký objekt, tak si zavolám "testData" třídu, (metodu get() nebo createAndSave()) a ta mi testovací objekt vrátí. Buď to může být objekt s hodnotami nastavenými v "testData" třídě nebo s hodnotami přebranými ze šablony - tím mám možnost si nastavit určité atributy nebo i celé objekty dle svých potřeb.

Ještě musím dodat, že jsme na našem projektu rozlišovali dva typy objektů - jeden typ objektu byl pouze pro čtení a byl vždy součástí testovací databáze. Typicky se jedná o číselníky - ty se nemění, ty se můžou pouze číst. Druhý typ objektu je normální objekt, který je nejdříve potřeba vytvořit a uložit do databáze.

Dle mého názoru tento přístup k vytváření testovacích objektů řeší většinu nevýhod zmíněných v předchozích způsobech inicializace. Sami jsme to otestovali na projektu, na kterém dělalo cca 15 programátorů a kde byl velmi košatý a složitý datový model. Snad jediná nevýhoda tohoto a podobných řešení je ta, že vkládaná data do databáze v rámci testů nejsou nikdy komitnuta, takže nemáme možnost si prohlédnout co skutečně v databázi je, např. během ladění.

Tento nápad nebyl pouze z mé hlavy, ale rád bych uvedl spoluautora Pavla Jetenského, který o našem řešení mluvil na letošním jOpenSpace, přednáška Lepší než Dbunit.

Samotný kód určitě napoví více než nějaká slova, tak přikládám jednotlivé třídy včetně ukázek použití.


AbstractReadTestData:

/**
* Abstraktni trida pro nacitani testovacich dat.
*
* <p>Tato trida nabizi pristup k jiz existujicim datum v DB (napr. registry).
* Pokud chcete testovaci data i vytvaret, pak je nutne pouzit tridu {@link AbstractCreateTestData}.
*
*/
public abstract class AbstractReadTestData<BO extends IIdentifiable> {

private static final String SET_METHOD_PREFIX = "set";
private static final String GET_METHOD_PREFIX = "get";

protected final Calendar now = Calendar.getInstance();

/** Mapa objektu, se kterymi se pracovalo. [ID objektu, BO] */
protected Map<Serializable, BO> dataMap = new TreeMap<Serializable, BO>();

/**
* Constructor
*/
protected AbstractReadTestData() {
}

/**
* Metoda vrati prvni (libovolnou) instanci test. dat z DB.
* Pokud dosud zadna takova instance nebyla vytvorena, tak je vyhozena vyjimka.
* Data v DB musi existovat.
*
* @return prvni (libovolnou) instanci test. dat
* @throws IllegalStateException pokud DB neobsahuje data pozadovaneho typu
*/
public BO get() {
BO data;
if (dataMap.isEmpty()) {
data = getExistingRegistryEntity(getDataDao());

if (data != null) {
dataMap.put(data.getId(), data);
}
else {
throw new IllegalStateException("There is no test data for loading.");
}
}
else {
data = dataMap.values().iterator().next();
}

return data;
}

/**
* Metoda vrati BO z DB.
*
* @param dataDao DAO pro pristup k datum
* @return BO nebo null pokud neexistuje
*/
protected BO getExistingRegistryEntity(IPlainDaoSupport<BO> dataDao) {
BO byId = null;
List<BO> findList = dataDao.findList();
if (findList.size() > 0) {
byId = findList.get(0);
}

return byId;
}

/**
* Metoda vyresetuje mapu testovacich dat, se kterymi se dosud pracovalo.
*/
public void cleanTestDataCache() {
dataMap.clear();
}

/**
* Metoda je urcena k implementaci v podtridach a vraci referenci na DAO
* pro nacitani dat z DB (a pripadne manipulaci s daty).
*
* @return DAO pro pristup k datum
*/
protected abstract IPlainDaoSupport<BO> getDataDao();

/**
* Metoda automaticky najde vsechny atributy (fieldy) tridy typu {@link Calendar}
* a nastavi jim aktualni datum a cas (pokud jsou null).
*
* @param data BO
*
* @throws UnsupportedOperationException pokud neni mozne nastavit pres setter Calendar
*/
protected void fillAllCalendarFields(IIdentifiable data) {
Method[] methods = data.getClass().getMethods();
for (Method method : methods) {
try {
Class<?>[] parameterTypes = method.getParameterTypes();
if (method.getName().startsWith(SET_METHOD_PREFIX)
&& parameterTypes.length == 1
&& Calendar.class.isAssignableFrom(parameterTypes[0])) {

// nastavime aktualni cas pouze pokud neni nic nastaveno
String getterMethodName = method.getName().replaceFirst(SET_METHOD_PREFIX, GET_METHOD_PREFIX);
Method getter = data.getClass().getMethod(getterMethodName);
Object value = getter.invoke(data);

if (value == null) {
method.invoke(data, this.now);
}
}
}
catch (Exception ex) {
throw new UnsupportedOperationException("Cannot automatically setup calendar method "
+ method.getName() + " of " + data.getClass().getName() + " to default not null value", ex);
}
} //end-for
}

}



AbstractCreateTestData:

/**
* Abstraktni trida pro vytvareni testovacich dat.
*
*/
public abstract class AbstractCreateTestData<BO extends IIdentifiable>
extends AbstractReadTestData<BO> {

/**
* Metoda vrati prvni (libovolnou) instanci test. dat (BO), ktera jiz byla drive vytvorena.
* Pokud dosud zadna takova instance nebyla vytvorena, tak se vytvori objekt s implicitnima hodnotama.
*
* <p><b>Pozor, tato metoda nenacita jiz existujici data z DB, pouze z interni cache po
* naslednem vytvoreni.</b>
*
* @return prvni (libovolnou) instanci BO - bud primo z DB
* @see #createAndSave
*/
final public BO get() {
BO data;
if (dataMap.isEmpty()) {
//vytvorime a ulozim impl. data
data = createAndSave();
}
else {
data = dataMap.values().iterator().next();
}
return data;
}

/**
* Metoda v databazi vytvori instanci, naplnenou vychozimi implicitnimi testovacimi daty.
* Instanci prideli nove logicke id s DB sekvence.
*
* @return BO s implicitnimi daty
*/
final public BO createAndSave() {
return createAndSave(null);
}

/**
* Metoda v databazi vytvori novou instanci test dat,
* naplnenou kombinaci vychozich testovacich hodnot a hodnot ze vstupni sablony.
*
* @param data Sablona pro vytvoreni test. objektu
* @return vytvoreny BO nebo null, pokud se nic nevytvori
*/
final public BO createAndSave(BO data) {
//vytvorime novy test. objekt
BO createdBo = create(data);

//ulozim je do DB a do cache
if (createdBo != null) {
getDataDao().save(createdBo);
dataMap.put(createdBo.getId(), createdBo);
}
return createdBo;
}

/**
* Metoda vytvari (neuklada!) novy testovaci objekt.
* Metoda je zodpovedna za vytvoreni nove instance objektu s nastavenim vsech "not-null" atributu, vcetne
* reference na dalsi objekty.
*
* <p>Vsude, kde je nektera z vlastnosti parametru sablony <i>data</i> nenulova, je pouzita pro novy objekt.
* V ostatnich pripadech se pouzije <i>nejaka</i> vychozi hodnota.
*
* @param data Sablona pro vytvoreni test. objektu
* @return nova testovaci data (BO)
*/
protected abstract BO create(BO data);
}



Aby byl přehled základních tříd kompletní, tak uvádím i rozhraní IIdentifiable, které používáme jako rozhraní pro všechny naše doménové objekty:

public interface IIdentifiable<T extends Serializable> {

/**
* Identifikator.
*
* @return Vraci identifikator objektu.
*/
T getId();

/**
* Metoda zjistuje, jestli ma objekt <i>id</i>.
*
* @return Vraci <tt>true</tt>, pokud ma objekt korektni id, jinak <tt>false</tt>.
*/
boolean isIdentified();
}



A nakonec ukázka použití - jsou použity doménové objekty IPersonBo a IRoleBo, vztah je takový, že osoba (Person) může mít jednu roli (Role). Nejdříve "testData" třídy:

public class PersonReadTestData extends AbstractReadTestData<IPersonBo> {

@Autowired
private IPersonDao personDao;

protected IPlainDaoSupport<IPersonBo> getDataDao() {
return personDao;
}
}



public class PersonCreateTestData extends AbstractCreateTestData<IPersonBo> {

@Autowired
private IPersonDao personDao;

@Autowired
private RoleCreateData roleCreateData;

protected IPersonBo create(IPersonBo data) {
if (data == null) {
data = personDao.createNewInstance();
}

if (StringUtils.isEmpty(data.getName())) {
data.setName("Petr");
}
if (StringUtils.isEmpty(data.getSurname())) {
data.setSurname("Kolousek");
}
if (data.getRole() == null) {
data.setRole(roleCreateData.createAndSave());
}

return data;
}

protected IPlainDaoSupport<IPersonBo> getDataDao() {
return personDao;
}
}



public class RoleCreateTestData extends AbstractCreateTestData<IRoleBo> {

@Autowired
private IRoleDao roleDao;

protected IRoleBo create(IRoleBo data) {
if (data == null) {
data = roleDao.createNewInstance();
}

if (StringUtils.isEmpty(data.getName())) {
data.setName("READ");
}

return data;
}

protected IPlainDaoSupport<IRoleBo> getDataDao() {
return roleDao;
}
}


... a pak použití v rámci testů:

public class CreateDataTest extends AbstractTransactionalJUnit4SpringContextTests {

@Autowired
private PersonCreateTestData personCreateData;

@Autowired
private RoleCreateTestData roleCreateData;

@Before
public void cleanCache() {
personCreateData.cleanTestDataCache();
roleCreateData.cleanTestDataCache();
}

@Test
public void testCreatingNewData() {
//vytvorime zaznamy v DB
IPersonBo person = personCreateData.createAndSave();
assertThat(person, is(notNullValue()));
assertThat(person.getName(), is("Petr"));
assertThat(person.getSurname(), is("Kolousek"));
assertThat(person.getRole(), is(notNullValue()));
assertThat(person.getRole().getName(), is("READ"));
assertThat(countRowsInTable("T_PERSON"), is(1));

IPersonBo retPerson = personCreateData.get();
assertThat(retPerson, is(person));
}

@Test
public void testCreatingNewDataWithGet() {
//vytvorime zaznamy v DB
IPersonBo person = personCreateData.get();
assertThat(person, is(notNullValue()));

IPersonBo retPerson = personCreateData.get();
assertThat(retPerson, is(person));
}

@Test
public void testCreatingExistingData() {
//vytvorime roli podle sablony
IRoleBo roleTempl = new RoleBoImpl();
roleTempl.setName("EXEC");
IRoleBo role = roleCreateData.createAndSave(roleTempl);
assertThat(role, is(notNullValue()));

//vytvorime person ze sablony
IPersonBo personTempl = new PersonBoImpl();
personTempl.setName("Jan");
personTempl.setSurname("Vavra");
personTempl.setRole(role);

IPersonBo person = personCreateData.createAndSave(personTempl);
assertThat(person, is(notNullValue()));
assertThat(person.getName(), is("Jan"));
assertThat(person.getSurname(), is("Vavra"));
assertThat(person.getRole(), is(notNullValue()));
assertThat(person.getRole().getName(), is("EXEC"));
assertThat(countRowsInTable("T_PERSON"), is(1));

IPersonBo retPerson = personCreateData.get();
assertThat(retPerson, is(person));
}

}

11. května 2009

GUTs = good unit tests

K problematice testování jsem četl výborný článek (1, 2) na JavaWorld, který všem vřele doporučuji - nejen kvůli obsahu, ale i kvůli množství odkazů na další články a zajímavé knihovny.

K obvyklým a častým "best-practices" (např. JUnit best practices) bych ještě přidal z mých zkušeností následující:

  • testovací kód by měl splňovat stejné kvalitativní nároky jako produkční kód. Místo jedné dlouhé testovací metody, která navíc ještě obsahuje duplicitní kód střídající se v jedn. testovacích metodách, je vhodné společný kód vyčlenit do separatních metod nebo i tříd, aplikovat pravidla refactoringu, když už kód "začne smrdět" (1, 2).
  • testy jsou ukázkou použití produkčního kódu, testy jsou součástí popisu API aplikace.
  • jednotlivé testy musí být na sobě nezávislé, pořadí spouštění testů může být libovolné
  • otestovat lze (skoro) všechno, jen je potřeba najít rovnováhu mezi náročností napsání testů a jejich přidanou hodnotou. Nesnažit se tedy za každou cenu dosáhnout 100% pokrytí produkčního kódu testy, ale spíše se držet pravidla 20/80, kdy otestováním 20% kódu otestujeme 80% funkcionality.
  • testy nesmí mít žádné vedlejší efekty, např. přidaný záznam v DB.
  • při psaní testů se nespoléhat na konkrétní prostředí (Locale, adresář na lokálním disku apod.), protože testy se mohou spouštět kdekoliv.

Co mě ale v uvedeném článku nejvíce zaujalo byla knihovna hamcrest. Při psaní testů jsem měl pořád problémy se psaním smysluplných komentářů do assertů a navíc mi ty všechny kontroly nepřisly úplně přehledné, když toho bylo více. Toto vše řeší hamcrest díky jednoduchému a výstižnému API - na ukázky a možnosti se podívejte do tutorialu nebo do uvedeného článku.

14. května 2008

Máte čas na unit testy?

Pokud bych si měl hned sám odpovědět, tak dle mého názoru moc času není. Není z jednoho hlavního důvodu a to jsou peníze resp. náklady plynoucí z času, který bude věnován psaní unit testů. Hned na začátku také musím dodat, že celý článek je o "normálních" jednorázových projektech, které nejsou nějak mission-critical. U vývoje produktu (tj. aplikace, kterou zabalím a pošlu zákazníkům nebo aplikace, kterou opakovaně dodávám) předpokládám, že bude prostor pro psaní testů mnohem větší.

Ještě jsem nedělal ve firmě, kde by se počítalo s tím, že N dní budu vyvíjet produkční kód a dalších N dní budu mít vyhrazeno na testování (pozn. píši to sice v pořadí produkční kód, testy, ale klidně je možné si představit přístup opačný - TDD). Takto to často prezentují agilní přístupy k vývoji softwaru, ale v praxi je dle mého názoru toto neuskutečnitelné, aspoň tedy v rámci projektového vývoje, protože poměr nákladů na psaní unit testů vs. přínosy z toho plynoucí nejsou zrovna moc pozitivní.

V žádném případě nejsem odpůrce psaní testů, ale realita mě nějak naučila se na tu problematiku dívat i z jiného pohledu než jen vývojářského. Vidím spoustu výhod ze psaní testů, zejména následující dva:

  • zajištění požadované kvality produkčního kódu
  • kvalitou se nemyslí jen kód bez chyb, ale i takové věci, jako kvalitní architektura aplikace, dobrá dekompozice problému apod. Pokud chci psát efektivně testy, tak pak potřebuji mít i dostatečně kvalitní produkční kód.

Samozřejmě bez testů to nejde, nedovedu si představit, že bych takto odevzdal aplikaci zákazníkovi. Ale na jaké testy se zaměřit, aby přínosy a náklady byly v rozumné rovnováze? Dle mých zkušeností jsem asi nejlepší poměr přínosy vs. náklady zaznamenal v těchto případech:
  • znovupoužitelný kus kódu (knihovna, modul)
  • stěžejní aplikační logika, např. jádro aplikace
  • doménové objekty - equals. Na to jsou již testy napsané, takže je jen použít. Přínos možná malý, ale ušetří spoustu času při hledání chyb s kolekcemi.
  • při použití ORM nástroje testovat základní CRUD operace nad objekty. Pro tyto účely jsem si napsal sérii testů, protože je to stejné pořád dokola a je dobré vědět, že práce s DB na té základní úrovni funguje jak má. Spíše je to testování ORM mapování jako celku, než jen samotného procesu ukládání dat do DB.

Naopak mám negativní zkušenost z Mock (jMock, EasyMock) testů servisních a jiných interních rozhraní v aplikaci. Přínosem určitě bylo to, že mi testy drží strukturu aplikace pohromadě, ale velkou nevýhodou byla ztráta flexibility - kdykoliv jsem provedl nějaký refaktoring, tak jsem musel měnit i tyto testy. Testy se musí měnit vždy s ohledem na změny produkčního kódu, ale v oblasti rozhraní se mění a přidává celkem hodně a dle mého názoru výhody nepřevažují nevýhody.

Speciální kapitolou je testování GUI. Již jsem se na toto téma jednou rozepsal, kde jsem porovnával nástroje pro unit testy GUI. Bohužel je dost často rychlejší tedy levnější nechat GUI testy provést manuálně, než testy vytvořit a testovat automaticky. Většina si hned asi v duchu řekne, že je to možná levnější během vývoje, ale z pohledu nasazení, údržby a podpory aplikace to už neplatí. Toto je asi pravda, ale těžko se to prosazuje v průběhu vývoje. Úplně nejlepší je zapojit do testování zákazníka. Snažíme se vyvíjet iteračním způsobem a vždy na konci každé iterace dáme hotovou část aplikace zákazníkovi, který to následně připomínkuje.

Mě by hodně zajímalo, zda můj pohled na věc odráží realitu současného vývoje aplikací nebo zda jsem mimo. Znám spoustu firem resp. kolegů, kteří ještě nenapsali ani čárku testovacího kódu, ale na druhé straně znám i firmy, kde se na testování klade velký důraz. Já bych sám rád testoval určitě více než doposud, ale ekonomické tlaky od obchodníků, projekt managerů a vedoucích mi to moc nedovolují.

25. října 2007

Testovací frameworky pro webové GUI

Rozhodli jsme se, že posuneme naše interní testování aplikací zase o kousek dále, a že vybereme nějaký testovací framework pro GUI webových aplikací.

Zatím jedinou zkušenost, co se týká testování web GUI, jsem měl s jWebUnit. Již jsem tuto knihovnu delší dobu nepoužíval, ale myslím si, že by se v dnešní době dalo najít lepší řešení. Ono je to tedy těžké napsat "lepší řešení", protože každý má jiné požadavky a cíle testování - někdo upřednostní GUI klienta na vytváření testů, někdo má rád programové vytváření testů přes API, někdo chce zaměřit na testování kompatibility mezi prohlížeči atd. Přesněji řečeno se tedy snažím "zmapovat trh" s testovacími frameworky a jeden vybrat a ten zkusit.

Já jsem věnoval zhruba půl dne tomu, abych si udělal hrubý obrázek o současných možnostech testovacích frameworků pro web GUI. V rychlosti jsem prostudoval Canoo WebTest, Selenium a Jameleon - ty jsem nominoval do užšího výběru na základě referencí od známých nebo podle toho, co jsem se dočetl na webu.
Znovu chci zdůraznit, že jsem vše procházel velice hrubě - pročetl jsem si základní dokumentaci, vyzkoušel dostupné nástroje a napsal 1-2 zkušební testíky. Nic víc. Pokud se mnou v nějakém bodě hodnocení nebudete souhlasit nebo naopak by jste něco doplnili, tak budu jedině rád.

Pro úplnost ještě uvádím odkaz na přehled všech možných testovacích knihoven a frameworků pro Javu.

Canoo WebTest

Plusy:
  • testovaní bez nutnosti použití webového prohlížeče

  • ovládání pomocí ANTu - Canoo definuje ANT tasky, kterými je možné vše ovládat (hlavně spouštět testy)

  • možnosti kontroly PDF a Excel dokumentů

  • dokumentace je rozsáhlá a přehledná

  • velice intuitivní, testy může vytvářet i neprogramátor

Mínusy:
  • vše je nutné definovat ručně přímo v XML, žádné podpůrné grafické rozhraní

  • Canoo nesimuluje reálného uživatele přes prohlížeč, pouze se víceméně zajímá o čisté HTML (to může být i výhoda pro určité typy testů)

  • může být rozdílné chování z pohledu Canoo a reálného uživatele (např. různá implementace Javascriptu v prohlížeči a v této knihovně)



Selenium

Plusy:
  • simuluje reálného uživatele

  • možnost testování kompatibility webové aplikace s prohlížeči

  • Selenium IDE (plugin do Firefox) - přes toto IDE je možné vytvářet testy (manuálně nebo nahráváním); velice intuitivní nástroj, vhodný i pro neprogramátory. Tento plugin z mého pohledu nemá chybu, parádní integrace s Firefoxem.

  • testy je možné vytvářet nahráváním pomocí Selenium IDE

  • možnost rozšiřování základní funkcionality pomocí extensions

  • ANT tásky a možnost spouštění testů z příkazové řádky

  • dokumentace, podpora

  • možnost vytváření testů přímo z jazyka Java případně z jiných jazyků (.NET, PHP, Perl, ...)

Mínusy:
  • měl jsem malé problémy se spouštěním testů z příkazové řádky, které jsem nejdříve vytvořit přes IDE nahráním

  • testovaná aplikace musí být spuštěna před vlastním testováním



Jameleon

Plusy:
  • asi nejvíce rozsáhlé a robustní řešení na testování. Je možné využívat formou pluginů jiné webové testovací frameworky - Selenium, jWebUnit, ...

  • není jen pro testovaní web GUI, ale obecně pro testování čehokoliv

Mínusy:
  • asi nejstrmější "learning-curve" z uvedených frameworků. U předchozích jsem měl hned pocit, že vím, jak s tím pracovat, tady to tak rychlé nebylo.

  • poslední release byl v prosinci 2006, od té doby nic nového (navíc poslední uvolněná verze není stabilní). Odkazy na archiv mailing listu hlásí chybu.

  • mam pocit, ze Jameleon je základní framework pro testování, který je potřeba si ještě rozšířit pro své konkrétní potřeby, aby se jeho potenciál maximálně využil (toto ale může být i výhoda)


Vítězem prvního kola se u mě stal framework Selenium. Rozhodla jednoduchost vytváření testů (která ale není vyvážena tím, že by to bylo slabé na funkcionalitu) a možnost vytváření testů neprogramátory.