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