21. dubna 2010

Programátorské konvence

Při procházení cizího kódu určitě každý ocení, když kód vypadá tak, jako kdybychom ho psali sami - stejné formátování kódu, stejná jmenná konvence pro pojmenování tříd a metod, stejná adresářová struktura projektu apod.

Proto jsem často překvapen, že na tyto "pravidla" se neklade potřebný důraz, i s ohledem na to, že údržba aplikace je mnohdy náročnější (časově, nákladově) než samotná realizace. Když už jsem někde pravidla viděl, tak mě zase překvapilo, jak jsou mnohdy složitá.

Pro mě je hlavním dokumentem "Code Conventions for the JavaTM Programming Language", který popisuje základní formátovací a syntaktická pravidla pro programování v jazyce Java. Nevidím tedy žádný důvod, proč vytvářet vlastní (nová) pravidla. Je určitě možné některé detaily změnit nebo upřesnit (např. délka řádky, kódování souborů, hlavičky tříd a metod, ...), ale nemá smysl měnit základní myšlenky dokumentu.

Kromě tohoto dokumentu považuji za programátorské konvence i další dokumenty jako např.:

  • Adresářová struktura projektů
  • Best practices - popis často řešených problémů
  • Názvosloví objektů a vzorů
  • Standardní architektura

Při návrhu těchto konvencí je potřeba myslet na to, že není snahou vytvořit 100 stránkový podrobný manuál, ale vytvořit nějaké rozumné mantinely a pravidla pro vývoj aplikace, které budou všem srozumitelná. Je na každém, jaké oblasti bude chtít pokrýt nějakými pravidly.

Já osobně vidím třeba velký přínos v tom, pokud názvy tříd a metod splňují nějaká pravidla, např. že metoda, která hledá data začíná find nebo že názvy perzistentních tříd končí na DAO. Pak mohu mnohem jednodušeji z jednoho místa nastavit transakce nebo zabezpečení aplikace.

12. dubna 2010

Udělátko na vytváření mock objektů

Pro vytváření mock objektů používám knihovnu Mockito. Pokud mám testovaný objekt O1, který obsahuje referenci na objekt O2, ze kterého chci vytvořit mock objekt, pak není žádný problém. Vytvořím si mock objektu O2, který pak nasetuji do objektu O1.

Co když ale potřebuji vytvořit mock objekt, který je volán až někde na desáté úrovni hierarchie volání? V tomto případě není možné se k cílovému objektu dostat přes hierarchii objektů a nasetovat mock objekt. Proto mě napadlo si udělat takové udělátko, které to dokáže.

Uvedená funkce má následující omezení:
- třídy resp. beany musí být inicializovány pomocí Spring kontejneru
- aby bylo možné najít beanu požadovaného typu, tak daného typu musí být právě jedna
- pro vytváření mock objektů se používá knihovna Mockito (ale není žádný problém to upravit na libovolnou jinou mockovací knihovnu)

Zdrojový kód:

import static org.mockito.Mockito.spy;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.test.util.ReflectionTestUtils;

...


/**
* Umoznuje mockovat realny Spring bean a tim si upravit cilovy objekt dle sve potreby.
*
* <p><b>Pokud cilovy objekt (field) bude AOP proxy, pak se bude vytvaret mock nad "skutecnym" objektem
* a ne proxy - vysledne volani tohoto objektu nebude tedy pres AOP proxy, ale primo.</b>
*
* @param bf Bean factory pro pristup ke Spring kontejneru
* @param beanClass Typ Spring beany, kterou chceme v kontejneru najit a u ktere chceme zmenit
* nejaky field a misto toho podstrcit mock instanci
* @param fieldName Nazev fieldu (pristup k fieldu je pres Java reflexi)
* @param spyCallback Rozhrani pro implementaci mocku
* @param <BC> Typ beanu, ktery obsahuje nejaky field typu FC, u ktereho chceme menit chovani
* @param <FC> Typ objektu fieldu
* @return mock objekt (hodi se napriklad pro overeni volani metod, vstupnich parametru metod atd.
* @see <a href="http://mockito.googlecode.com/svn/tags/latest/javadoc/org/mockito/Mockito.html#13">Spying on real objects</a>
*/
public static <BC, FC> FC spyBean(ListableBeanFactory bf,
Class<BC> beanClass,
String fieldName,
SpyCallback<FC> spyCallback) {
return TestUtils.spyBean(bf, beanClass, fieldName, null, spyCallback);
}


/**
* Umoznuje mockovat realny Spring bean a tim si upravit cilovy objekt dle sve potreby.
*
* <p><b>Pokud cilovy objekt (field) bude AOP proxy, pak se bude vytvaret mock nad "skutecnym" objektem
* a ne proxy - vysledne volani tohoto objektu tedy nebude pres AOP proxy, ale primo.</b>
*
* @param bf Bean factory pro pristup ke Spring kontejneru
* @param beanClass Typ Spring beany, kterou chceme v kontejneru najit a u ktere chceme zmenit
* nejaky field a misto toho podstrcit mock instanci
* @param fieldName Nazev fieldu (pristup k fieldu je pres Java reflexi)
* @param fieldClass Typ fieldu, muze byt null. Pokud bude definovan a field bude null, tak pak se
* automaticky vytvori nova instance objektu.
* @param spyCallback Rozhrani pro implementaci mocku
* @param <BC> Typ beanu, ktery obsahuje nejaky field typu FC, u ktereho chceme menit chovani
* @param <FC> Typ objektu fieldu
* @return mock objekt (hodi se napriklad pro overeni volani metod, vstupnich parametru metod atd.
* @see <a href="http://mockito.googlecode.com/svn/tags/latest/javadoc/org/mockito/Mockito.html#13">Spying on real objects</a>
*/
@SuppressWarnings("unchecked")
public static <BC, FC> FC spyBean(ListableBeanFactory bf,
Class<BC> beanClass,
String fieldName,
Class<? extends FC> fieldClass,
SpyCallback<FC> spyCallback) {
//najdeme beanu daneho typu (danemu typu musi odpovidat prace jedna beana)
Map beanMap = bf.getBeansOfType(beanClass);

if (beanMap.size() != 1) {
throw new IllegalStateException("Zadanemu typu " + beanClass + " odpovida vice nebo zadny bean(u).");
}


//mame (pravdepodobne) proxy objekt - pokud ano, tak najdeme cilovy objekt
BC proxy = (BC) beanMap.values().iterator().next();

BC target = proxy;
if (AopUtils.isAopProxy(proxy)) {
try {
target = (BC) ((Advised)proxy).getTargetSource().getTarget();
}
catch (Exception ex) {
throw new RuntimeException("Problem pri ziskavani ciloveho objektu proxy objektu.", ex);
}
}


//ziskame cilovy objekt, ktery chceme mockovat
FC field = (FC) ReflectionTestUtils.getField(target, fieldName);

if (field == null && fieldClass == null) {
throw new IllegalStateException("Nactena hodnotu fieldu '" + fieldName + "' nemuze byt null.");
}
else if (field == null) {
//vytvorim automaticky instanci fieldu
try {
field = fieldClass.newInstance();
}
catch (Exception ex) {
throw new IllegalArgumentException("Nepodarilo se vytvorit instanci tridy '" + fieldClass + "'.");
}
}
else {
//field mame nacteny - kontrola, zda to opet neni AOP proxy (potrebuji mockovat skutecny cilovy objekt)
if (AopUtils.isAopProxy(field)) {
try {
field = (FC) ((Advised)field).getTargetSource().getTarget();
}
catch (Exception ex) {
throw new RuntimeException("Problem pri ziskavani ciloveho objektu fieldu.", ex);
}
}
}


FC spyObjekt = spy(field);

//klient musi urcit cilove chovani objektu
spyCallback.spy(spyObjekt);

//upraveny objekt nasetujeme zpatky do ciloveho objektu
ReflectionTestUtils.setField(target, fieldName, spyObjekt);

return spyObjekt;
}


/**
* Rozhrani pro implementaci mocku nad instanci objektu.
*
* @param <T> Typ objektu, u ktereho chceme zmenit chovani (vytvorit mock)
* @author <a href="mailto:petr.juza@marbes.cz">Petr Juza</a>
* @see <a href="http://mockito.googlecode.com/svn/tags/latest/javadoc/org/mockito/Mockito.html#13">Spying on real objects</a>
*/
public interface SpyCallback<T> {

/**
* Metoda pro implemtaci "noveho chovani" vstupniho mock objektu.
*
* <p>Na vstupnim objektu byla jiz zavolana metoda {@link org.mockito.Mockito#spy}, takze
* staci jen implementovat upravu chovani zadaneho objektu.
*
* @param spyObjekt Objekt
*/
void spy(T spyObjekt);
}


Pro názornost uvedu příklad:
Bean typu AaSavePripad.class má atribut (field) se jménem "typPripadService" typu TypPripadService. Já pro test potřebuji, aby volání metody TypPripadService.mohuVytvoritPripadTypu vracelo vždy true.

//potrebuji, aby volani typPripadService.mohuVytvoritPripadTypu bylo TRUE (z duvodu dokonceni ulozeni pripadu)
TypPripadService spyObjekt = TestUtils.spyBean(applicationContext, AaSavePripad.class, "typPripadService",
new SpyCallback<TypPripadService>() {
@Override
public void spy(TypPripadService spyObjekt) {
doReturn(true).when(spyObjekt).mohuVytvoritPripadTypu(eq(odpad.getPripad().getTyp_Pripad()), anyInt());
}
});



K tomuto tématu jsem našel článek od Dagiho "Springframework mockujeme beany", který řeší problém trochu jiným způsobem. Mě se na uvedeném řešení moc nelíbí, že pokud chci upravit chování nějaké metody, tak si musím vytvořit celý mock objekt a musím vše konfigurovat. Navíc nemám možnost mít různé podoby resp. chování jednoho mock objektu pro různé testy v rámci jedné testovací třídy (protože XML kontext se definuje na úrovni třídy a ne jednotlivých testů).

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.

Co používáte na testování? - výsledky

Poslední anketní otázka byla zaměřena na testování - zajímalo mě, jak moc se používá něco jiného než jUnit.

Výsledky jsou následující (bylo možné hlasovat pro více možností):

  1. jUnit (84%)
  2. TestNG (16%)
  3. něco jiného (1%)

Výsledky pro mě nejsou žádným překvapením, jen by mě upřímně zajímalo, co se skrývá pod tím "něco jiného" - nějaká další knihovna na testování nebo nějaký interní framework?

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);
}