9. listopadu 2010

Hibernate: rozdílné výsledky HQL a Criteria API?

Již je to nějaký čas, co jsem řešil problémy s Hibernate dotazy a v rámci ladění jsem už zkoušel všechno možné i nemožné a podařilo se mi, že jsem měl "stejné" dva dotazy, ale každý vracel jiné výsledky.

První dotaz je napsán pomocí Criteria API a v dané úloze správně nevrátil žádnou hodnotu. Druhý dotaz je napsán pomocí HQL a špatně najde jeden záznam (jednoho poplatníka).


Criteria criteria = createCriteria();
   
criteria.add(Restrictions.gt(DOaa_odpad_poplatnik.PLATNOST_OD, param.getDatum_posl_aktualizace_aa()));
    
List list1 = findByCriteria(criteria);

    



String hql = "select poplatnik from " + DOaa_odpad_poplatnik.class.getSimpleName() + " poplatnik "
        
+" where platnost_od > :datum";
    
Query query2 = createQuery(hql);
    
query2.setDate("datum", param.getDatum_posl_aktualizace_aa());
    
List list2 = query2.list();

Musím se přiznat, že do teď netuším, jak je to možné. Pokud někdo víte, budu rád, když se přiučím ...

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:

26. září 2010

Přijímací pohovory jsou vizitkou firmy

Z povahy mého stylu práce se často účastním různých přijímacích pohovorů či výběrových řízení. Přijde mi, že mnoho firem si neuvědomuje, že i takový přijímací pohovor je vizitkou resp. reklamou dané firmy. Přeci to není jen o tom, že někdo hledá nějakou práci, ale i o tom, že nějaká firma chce najít toho nejlepšího spolupracovníka.

Hlavním důvodem k napsání tohoto příspěvku je časté překvapení z průběhu náboru nových spolupracovníků. Překvapuje mě toho více:

  • o smyslu vstupních testů zde nechci polemizovat, ale spíše nejsem jejich zastánce, protože si myslím, že mají malou vypovídající hodnotu. Teď neřeším, zda se mi to libí nebo nelíbí z pozice uchazeče, ale někdy i stojím na druhé straně barikády a mám možnost si vybírat spolupracovníky či rozhodovat o přijetí nových zaměstnanců.

  • pokud tedy vstupní testy ano, pak bych očekával, že budou nějak relevantní k mým odborným zkušenostem a k práci pro firmu. Co si mám myslet o otázkách typu "K čemu jsou dobré anotace?", "Kdy je vhodné použít databázový trigger?", "Porovnání rozhraní a tříd z pohledu dědičnosti" nebo otázky na Swing, když mě firma žádá o spolupráci na webovém projektu? Nikdy jsem takové testy neodmítl, vždy to beru jako takovou výzvu, ale ještě jsem se pro žádnou takovou firmu nerozhodl.

  • čekání na pohovor, odbíhání od pohovoru, vyřizování telefonátů apod. jsou věci, které mi vadí při jakémkoliv jednání a zde dvojnásob.

  • role pracovních agentur je pro mě často velmi záhadná, ale je víceméně jasné, že je jim jde jen o peníze. Z mého pohledu žadatele o práci jsem ještě nepřišel na žádnou výhodu, kterou mi agentury poskytují - ozvou se vždy jen tehdy, když něco potřebují, když mě chtějí poslat na nějaký pohovor, ale když já občas něco potřebuji nebo hledám, tak většinou bez výrazného zájmu. Také mi vadí, že je v agenturách velmi velká fluktuace lidí. Pak mi každý půlrok volá někdo jiný, neexistuje žádné osobnější pouto.


Naštěstí převládají příjemné pohovory, kdy se cítím spíše jako možný budoucí partner než někdo, kdo žádá o práci. Nejednou se mi stalo, že jsme přeskočili technickou část pohovoru z důvodu přečtení mého blogu - to pak jsem vždy velmi potěšen.

Většina věcí je o lidech samotných, takže těžko soudit celou firmu podle pár lidí, se kterými se člověk potká, ale na druhou stranu také vím, že "špatní" mají tendenci spolupracovat se "špatnýma" a "dobří" s "dobrýma".

Ještě přidávám odkaz na dva pěkné články k tématu - Getting Hired, první a druhý díl.

Bude Oracle pro Javu přínosem? - výsledky

I'm back :-). Mám za sebou na dovolené a práci bohaté léto, takže jsem neměl moc času na psaní. Také se ale musím přiznat, že na mě padla tak trochu "autorská krize", a že jsem rád, že jsem za těch pár měsíců mohl nabrat novou energii a nová témata. I když práce na podzim bude hodně, tak i přesto bych rád sem tam něco napsal ...


Před pár měsíci skončila poslední anketa na tomto blogu s těmito výsledky (celkem hlasovalo 59 lidí):

  1. Ne (37%)
  2. Ano (30%)
  3. Nevím (20%)
  4. Nic se nezmění (11%)

Já osobně jsem hlasovalo pro "Ne" a myslím, že události v posledních měsících tomu jednoznačně napovídají. Oracle byla, je a bude firma primárně zaměřená na peníze. To je asi většina firem, jinak by bylo asi něco špatně, ale u jiných firem to není tak okaté, není to tak na první pohled vidět. Co jiného si mám myslet o žalobách na Android? Co si mám myslet o posledním vývoji JDK a Javy samotné? (pozn. do teď si pamatuji, jak se Sun dušoval po vydání Javy 5, že nyní budou každé dva roky nová verze JDK ...)

Je pravda, že se na celou věc koukám z pohledu normálního programátora, kterého asi nejvíce zajímá samotná Java a co s ní bude do budoucna. Kdybych se na to koukal jako nějaký manažer nebo obchodník, tak bych to asi viděl zase jinak. A nebo prostě má Oracle špatný marketing a pověst ve srovnání s ostatními podobnými hráči.

Na druhou stranu se musím přiznat, že od samotné Javy už toho moc neočekávám. Dnes samotná Java nic není. Javovská platforma je dnes takový ekosystém všech možných knihoven, frameworků a dynamických jazyků, které mají to společné, že mohou běžet v JVM. Proto nemá cenu do jazyka Java přebírat všechny možné vychytávky z jiných jazyků, ale spíše se snažit o udržení přehlednosti a jednoduchosti jazyka. Když se podívám na seznam plánovaných vlastností, tak já osobně bych nejvíce uvítal nativní podporu modularizace přímo na úrovni JVM a integraci knihovny JODA pro lepší práci s datumem a časem. Některé zbylé věci mi přijdou jako "nice to have" (např. strings in switch, Automatic resource management, The fork/join framework), ale jiné bych prostě do Javy už nedával (např. closures, Diamond operator, Annotations in more places).

Jsem toho názoru, že Java jako jazyk má nejlepší léta za sebou a nemá nyní cenu se snažit dohánět ostatní nové moderní jazyky. Já programuji v Javě skoro deset let a vždy jsem nejvíce oceňoval jednoduchost, přehlednost a stabilitu jazyka, širokou komunitu, velké množství informací a možnost volby a výběru a nikdy jsem si moc nestěžoval, že by se mi něco psalo nějak špatně, že by se to mohlo psát lépe.

Co ale vidím pozitivně je to, že Java tu bude ještě hodně dlouho, protože Oracle je na Javě výsostně závislý, a proto se nebojím toho, že by během pár let padla v zapomnění. Takže ještě pár let budu programovat a pak už snad budu rentiér :-).

24. května 2010

Kdo nemá twitter, není správný programátor

"Kdo nemá twitter, není správný programátor" - takový pocit jsem získal v poslední době, když pozoruji své kolegy v práci nebo lidi v Java komunitě. Pokud nemáš twitter, tak jsi out :). Je to samozřejmě přehnané, ale když se podívám na českou Java komunitu, tak je opravdu hodně lidí, kteří Twitter používají.

Nepřidávám se snad ani kvůli tomu, že chci být in, ale hlavně z toho důvodu, že to chci zkusit. Chci zkusit zjistit, zda je tato forma komunikace pro mě přínosem, chci tímto způsobem získat nové zajímavé informace, chci tímto způsobem i já sám s trochou přispět a upozornit na zajímavé články nebo myšlenky. Určitě ale nechci psát o mém soukromí, o tom, co právě dělám a o tom jak se mám. Rád bych, aby se vše týkalo vývoje a Javy.

Nevím, zda je to jen můj pohled, ale přijde mi, že v poslední době (cca půl roku zpátky) se nepíší blogy tak jako dříve. Těžko říci, čím to je - již není o čem psát? Jsou už blogeři unavení nebo mají již něco jiného na práci? Nahrazuje twitter klasické blogování? Myslím si, že asi od všeho něco. Já bych každopádně ještě nějaký čas rád psal, protože pořád cítím, že mi to více dává než kolik tomu dávám já.

Pokud mě tedy chcete následovat, pak mám účet pjuza.

21. května 2010

Auto-wiring a možná řešení při více beanech stejného typu

Pokud používáme auto-wiring (dále předpokládám auto-wiring podle typu), tak se nám lehce může stát, že máme více beanů (instancí) stejného typu. V tomto případě Spring vyhodí výjimku, protože nemá žádný návod, jak tuto situaci vyřešit.

Zejména v testech se mi toto stává velice často, protože mám bean produkčního kódu, ale pro testy chci použít implementaci určenou pouze pro testy.

Příklad:

context.xml
<bean id="b1" class="FooImpl" />

context-test.xml
<bean id="b2" class="FooTestImpl" />

@ContextConfiguration(locations = {"classpath:/context.xml", "classpath:/context-test.xml"})
public void FooTest() {
@Autowired
private Foo foo;
}


Mám několik možností, jak tento problém řešit:

  • nadefinuji druhý (testovací) bean se stejným ID jako produkční bean a jelikož se testovací bean inicializuje jako druhý (z tohoto důvodu je velice důležité pořadí inicializace souborů Spring konfigurace), tak se přepíše (override) a ve Spring kontejneru bude ve výsledku pouze jeden bean - v tomto příkladu FooTest. Stejně to bude fungovat i když u obou beanů vynechám atribut ID. Pak se ID generují automaticky (viz implementace BeanNameGenerator) a budou tedy také stejná.

  • použiji anotaci @Qualifier a poradím Springu, jakou beanu má přesně vybrat. Ovšem ještě musím upravit konfiguraci beanu b2.

    <bean id="b2" class="FooTestImpl">
    <qualifier value="test">
    </bean>

    @Autowired
    @Qualifier("test")
    private Foo foo;

    Také to jde bez úpravy konfigurace a bez použití tagu <qualifier>. Pokud tento tag není definován, pak defaultní chování je takové, že se vygeneruje qualifier s hodnotou ID daného beanu. Mohu tedy pak napsat následující (ovšem pro tento přístup spíše doporučuji použití anotace @Resource, která je pro to určena):
    @Autowired
    @Qualifier("b2")
    private Foo foo;

  • použití anotace @Resource je pro tento případ vhodnější nez @Qualifier, zejména z pohledu sémantiky obou anotací.
    @Resource(name = "b2")
    private Foo foo;

  • použití primary atributu. Tento atribut dostupný pro každý bean říká, že se jedná o primární (první, doporučený) bean pro auto-wiring, pokud bude více adeptů stejného typu. Tuto vlastnost jsem objevil až nedávno, protože zmínka o ní je celkem zapadlá v dokumentaci.

  • za určitých podmínek by bylo možné ještě použít atribut autowire-candidate u produkčního beanu b1 a nastavit ho na false. Tím pak bude pro auto-wiring určen jen bean b2.

Tak jednoduchý problém a tolik možností řešení :). Já osobně nejčastěji používám řešení s primary a nebo s přepisováním definice beanu.

16. května 2010

Jaká bude budoucnost Javy? - výsledky

Dnes skončila další anketa a to s těmito výsledky (celkem hlasovalo 77 lidí):

  1. Pozice Javy se moc měnit nebude (50%)
  2. To nejlepší má již Java za sebou (25%)
  3. Javu čekají světlé zítřky (19%)
  4. .Net časem získá převahu (14%)
  5. Budoucnost patří dynamickým jazykům (12%)
  6. Za 10 let si na Javu nikdo ani nevzpomene (6%)

Jako hlavní komentář mi dnes poslouží článek Dagiho Mračna, sluníčko a nebo smrádek a teploučko nad Javou. Sám bych to lépe nenapsal a s jeho názorem souhlasím.

Celkem mě překvapuje, že si tolik lidí myslím, že Java na tom bude pořád stejně. Mě se toto celkem špatně posuzuje, protože se soustředím jen na Javu a nemám moc času sledovat i jiné platformy, ale jeden příklad bych měl. Ve firmě nyní připravujeme novou generaci klienta našich aplikací (nyní jsou klienti programovány v Delphi) a nebylo celkem co řešit - vítězem se stal Silverlight od Microsoftu. Samozřejmě snaha byla o to, aby i klient byl v Jave, když celý server je Javovský, ale Java prostě v této oblasti zcela zaspala a JavaFX ve mě pořád nebudí takovou důvěru, aby jsme se nyní na ní mohli spolehnout na dalších 5, 10 let.

Sice si také nemyslím, že by se pozice Javy v nejbližších letech měla měnit, ale určitě je potřeba inovovat. Na závěr bych použil jeden předvolební slogan "Nezajímá mě zítřek, ale budoucnost". (snad jsem to napsal dobře :-) ).

Generování class diagramů - Maven plugin

Na konci minulého roku jsem psal o programovém generování class diagramů.

Dnes bych na tento článek navázal implementací Maven pluginu. Ve firmě jsme kompletně přešli na Maven, takže jsem byl nucen vhodně přepsat původní ANT skript.

Implementace byla jednoduchá, napsal jsem totiž Maven plugin pomocí ANTu (pozor, nemyslím využití Maven Ant pluginu).

Dále přikládám zdrojové kódy, které asi nepotřebují další komentáře:

pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cz.marbes.plugins</groupId>
<artifactId>daisy-maven-plugins</artifactId>
<version>3.12.0.11-SNAPSHOT</version>
</parent>
<groupId>cz.marbes.plugins</groupId>
<artifactId>generator-diagram-plugin</artifactId>
<packaging>maven-plugin</packaging>
<version>3.12.0.11-SNAPSHOT</version>
<name>generator-diagram-plugin Maven Mojo</name>
<description>Plugin na generovani class diagramu</description>


<build>
<plugins>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<version>2.3</version>

<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-tools-ant</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>

<configuration>
<goalPrefix>diagram</goalPrefix>
</configuration>
</plugin>
</plugins>
</build>


<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-script-ant</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.umlgraph</groupId>
<artifactId>doclet</artifactId>
<version>5.1</version>
</dependency>
<!-- Knihovna UmlGraph vyzaduje, aby byl vedle ni umisten tools.jar -->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.5.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
<!--<systemPath>${user.home}/.m2/repository/org/umlgraph/doclet/5.1/tools.jar</systemPath>-->
</dependency>
</dependencies>

</project>


diagram.build.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project name="generator-diagram-plugin">
<target name="generate">
<!-- vytvoreni vystupniho adresare -->
<mkdir dir="${outputDir}"/>
<delete dir="${outputDir}"/>
<mkdir dir="${outputDir}"/>

<!-- generovani class diagramu -->
<java classname="cz.marbes.plugins.maven.generator.diagram.ClassDiagramGenerator">
<arg value="${package}"/>
<arg value="${outputDir}"/>
<arg value="${srcDir}"/>
<arg value="${dotFile}"/>
</java>
</target>
</project>


diagram.mojos.xml:

<?xml version="1.0" encoding="UTF-8"?>
<pluginMetadata>
<mojos>
<mojo>
<goal>generate</goal>

<!-- this element refers to the Ant target we'll invoke -->
<call>generate</call>

<requiresProject>true</requiresProject>

<description>
Plugin na generovani class diagramu.

Plugin vyzaduje (bohuzel) dve nutna zla:
- musi byt lokalne nainstalovay Graphiz (http://www.graphviz.org). Cesta k '
dot' souboru se zadava pres property 'dotFile'
- knihovna tools.jar musi byt vedle knihovny UmlGraph v Maven repository
</description>
<parameters>
<parameter>
<name>outputDir</name>
<property>outputDir</property>
<required>true</required>
<readonly>true</readonly>
<expression>${outputDir}</expression>
<defaultValue>${project.build.directory}/diagram_output</defaultValue>
<type>java.lang.String</type>
<description>Cesta k vystupnimu adresari, do ktereho se bude generovat diagram.</description>
</parameter>
<parameter>
<name>srcDir</name>
<property>srcDir</property>
<required>true</required>
<readonly>true</readonly>
<expression>${srcDir}</expression>
<defaultValue>${project.build.sourceDirectory}</defaultValue>
<type>java.lang.String</type>
<description>Absolutni cesta k adresari projektu se zdrojovymi soubory, kde je zadany package,
napr. '
/Volumes/Obelix/projects/daisy/apl/aa/trunk/aa-core/src/main/java'.
</description>
</parameter>
<parameter>
<name>package</name>
<property>package</property>
<required>true</required>
<expression>${package}</expression>
<type>java.lang.String</type>
<description>Package, pro ktery se ma generovat class diagram,
napr. '
cz.marbes.daisy.modules.aa.wscommon.komu.v1_1_2'.
</description>
</parameter>
<parameter>
<name>dotFile</name>
<property>dotFile</property>
<required>true</required>
<expression>${dotFile}</expression>
<type>java.lang.String</type>
<description>Cesta ke graphviz DOT souboru (napr. '
/usr/local/bin/dot')</description>
</parameter>
</parameters>
</mojo>
</mojos>
</pluginMetadata>


Nakonec ještě přikládám použití pluginu:

<profile>
<id>generate-diagram</id>
<!--
Profil vyzaduje dve nutna zla:
- musi byt lokalne nainstalovay Graphiz. Cesta k 'dot' souboru se zadava pres property 'dotFile'
- knihovna tools.jar musi byt vedle knihovny UmlGraph v Maven repozitory

Priklad:
mvn process-classes -Pgenerate-diagram -DdotFile=/usr/local/bin/dot -Dpackage=cz.marbes.daisy.modules.aa.modules.pvs.ws.v2_0_0
-->
<build>
<plugins>
<plugin>
<groupId>cz.marbes.plugins</groupId>
<artifactId>generator-diagram-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>process-classes</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.5.0</version>
<scope>system</scope>
<systemPath>${user.home}/.m2/repository/org/umlgraph/doclet/5.1/tools.jar</systemPath>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</profile>


Stejně jako původní řešení, tak i Maven plugin má své omezení, ale pro interní použití se to dá překonat. Důležité je, že dokážeme generovat pěkné class diagramy k našemu kódu. Class diagramy dáváme zejména do dokumentace k našim webovým službám, protože to zvyšuje jejich čitelnost a použitelnost.

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

28. ledna 2010

Inicializace a plnění kolekce na jediném řádku

Při psaní testů (zejména při vytváření testovacích dat) rád používám "zkrácené" zápisy pro inicializaci a plnění kolekcí.

Každého asi napadne použití Arrays.asList metody:


List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");

To je krátké, elegantní, ale s jednou malinkou nevýhodou. Takto se dá vytvořit pouze seznam, ne např. množina (i když samozřejmě není problém vložit kolekci do kolekce a tedy vytvořit množinu ze seznamu).

Já kromě výše uvedeného ještě používám tento zápis (většinou, když potřebuji jednopoložkovou kolekci a není to seznam):

new ArrayList<String>() {{add("Larry");}}

Na první pohled možná trochu magické, ale když se to rozepíše do více řádků, tak je zřejmé, že se využívá inicializačního bloku. Navíc tento zápis se neomezuje jen na kolekce, ale na jakýkoliv objekt.

24. ledna 2010

Hibernate - řazení NULL hodnot

Dnes výjímečně nebudu publikovat svůj příspěvek, ale příspěvek mého současného kolegy Vaška Hrdiny. Řešený problém mi přišel natolik zajímavý, že jsem ho požádal o publikaci na mém blogu.

Řazení v Hibernate

Pro řazení přes Criteria API existuje metoda addOrder():
crit.addOrder(Order.asc(DOdatovy_objekt.POLE)); //vzestupně
crit.addOrder(Order.desc(DOdatovy_objekt.POLE)); //sestupně
pro HQL je možné použít klauzuli order by:
createQuery("from " + DOdatovy_objekt.class.getSimpleName() +  " as do order by do." + DOdatovy_objekt.POLE); //vzestupně 
createQuery("from " + DOdatovy_objekt.class.getSimpleName() +  " as do order by do." + DOdatovy_objekt.POLE + " desc"); //sestupně

Řazení NULL hodnot

Problém ale nastává pro řazení NULL hodnot. Pokud je políčko NULL, pak jej MSSQL řadí na první místo, Oracle oproti tomu jako poslední. Ne vždy můžeme použít řazení až na aplikačním serveru (např. při tisku velké sestavy, kdy musíme získávat data stránkovaně), přesto je třeba řadit na všech DB stejně.
Nepřišel jsem na vhodný způsob, jak toto vyřešit, hibernate JIRA obsahuje požadavek, který se tohoto týká. Jediný způsob, na který jsem byl schopný přijít je řešení přes formule.

Jedná se o to, že do mapovaného objektu přidáte políčko (stačí políčko, nemusí mít ani getter), které se naplní předem definovaným SQL příkazem (pozor, jedná se skutečně o SQL, je tedy třeba hlídat mezidatabázovou kompatibilitu). Podle něj se dá řadit. A pokud zajistíte, aby se v políčku vyskytovaly takové znaky, aby se řadilo podle Vašich potřeb (typicky NULLy půjdou jako první), je vyhráno.

Následující příklad ukazuje, jak řadit písmeno orientačního čísla tak, aby pokud není vyplněné, bylo první (tedy např. 16, 16a, 16b...):
@Entity
@Table(name = "adresa")
public class DOadresa extends cz.marbes.daisy.modules.x.mdo.DOGadresa {

@Formula("case when PISMENO_CO is null then ' ' else PISMENO_CO end")
private String pismenoCONotNull;
}

V kódu, který potřebuje takto řadit, pak stačí uvést:
crit.addOrder(Order.asc("pismenoCONotNull")); //pro Criteria API
createQuery("from " + DOdatovy_objekt.class.getSimpleName() + " order by pismenoCONotNull"); //pro HQL

Políčko pismenoCONotNull je pak normálně přístupné, jako kterékoliv jiné políčko, obsahuje buď mezeru nebo obsah pole PISMENO_CO.

1. ledna 2010

Jaký server nejčastěji používáte?

V poslední anketní otázce jsem se ptal, jaký aplikační server nejčastěji používáte. Celkem vás hlasovalo 124 s následujícími výsledky:

  1. Apache Tomcat (59%)
  2. GlassFish (11%)
  3. JBoss (9%)
  4. WebLogic (7%)
  5. Jetty (4%)
  6. IBM Websphere (3%)
  7. Jiný (2%)
  8. SpringSource dm Server (1%)

Já osobně se snažím všude používat Apache Tomcat, protože mi svoji jednoduchostí a funkcionalitou naprosto stačí a vyhovuje. Když jsem používal jiný server, tak to bylo většinou na přání zákazníka. Také mám zkušenosti s IBM Websphere serverem, protože jsme jeden projekt stavěli pomocí Websphere Integration Developeru.