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?