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.

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í.