Unit Testing PL/SQL v SQL Developeru / Problémy

V předchozím článku byla demonstrována tvorba jednoduchého unit testu s využitím nástroje zabudovaného přímo do SQL Developeru. Podívejme se nyní na několik problémů které použití tohoto nástroje v praxi přináší - jedná se jak o chyby (odstranitelné) tak i o důsledky některých rozhodnutí při návrhu systému (které jsou bohužel do systému zabudované).

Obávám se že tento článek bude vnímán jako jakási filipika proti řešení zabudovaného do SQL Developeru - tak míněn není. Cílem článku je poukázat na některé nevýhody které toto řešení má, a které nejsou zpočátku zřejmé. Což je ale situace ve které se nachází většina týmů které na začátku projektu rozhodují co pro psaní unit testů použijí - zda použijí to co na předchozím projektu nebo zda zkusí nějaký jiný nástroj.

V této pozici jsme totiž před několika měsíci byli i my, po zběžném odzkoušení různých řešení (SQL Developer, utPLSQL, PLUTO, atd.) jsme nakonec sáhli právě po SQL Developeru. Nevím zda bychom se dnes rozhodli stejně ...

Podívejme se nyní na stručný přehled potíží které jsou v SQL Developer unit testech skryty. Nejdříve se podívejme na problémy které jsou důsledkem návrhu celého systému, a jejich ostranění v dalších verzích SQL Developeru je tak velmi nepravděpodobné:

Co se týká chyb, není samozřejmě systém bez bugů. Pomíjím kosmetické chyby toho typu že se někde neukládá hodnota z průvodce (např. očekávaná návratová hodnota funkce) a uvedu zde jedinou, dle mého názoru celkem závažnou, chybu, ke které bohužel neexistuje elegantní workaround

Většina z nich je zmíněna v tomto příspěvku na diskusním fóru na oracle.com a u části z nich je uvedeno že jsou (nebo budou) v další verzi opraveny (ale nikoliv ty nejzávažnější / nejotravnější).

A nyní se na jednotlivé problémy podívejme blíže.

Absurdně strohý editor částí unit testu

Jestli vás při psaní unit testů bude něco štvát od samého počátku, bude to nepochybně editor ve kterém se vám otevírají jednotlivé části testů.

editor unit testů

Editor postrádá cokoliv kromě základní editace textu - zapomneňte na code completion, zapomeňte na cokoliv dalšího na co jste zvyklí ze zbytku SQL Developeru. O tom že se editor neustále otevírá jako malý modální dialog (tj. nemůžete se podívat např. na strukturu tabulky) ani nemluvě.

Editovat tedy můžete buď naslepo nebo si můžete kód zkopírovat do standardního PL/SQL editoru, provést změny a pak kód zase zkopírovat zpět.

Nicméně ačkoliv je to otravné, lze očekávat že se to v dalších verzích SQL Developeru změní. Prostě je to nová vlastnost, implementována byla "základní funkčnost" a na libůstky jako je pohodlný editor zatím nedošlo.

Nemožnost debuggingu

Tento problém je důsledkem faktu že celý framework je založen na užití anonymních bloků, jimiž jsou implementovány všechny části unit testu - startup, validace i teardown. Každé z těchto částí odpovídá samostatný anonymní blok PL/SQL kódu. Můžete si ho obalit např. do procedury, případně použít "advanced" vlastnosti testovacího frameworku (knihovny), ale na podstatě problému to nic nemění.

V PL/SQL totiž anonymní bloky nelze rozumně debugovat - krokovat, nastavovat breakpointy, apod. Technicky se tedy přesouváte o několik úrovní níže, protože jediný možný způsob debuggingu je vkládání mnoha volání DBMS_OUTPUT a přiznejme si že to není zrovna efektivní a pohodlný způsob.

Bohužel na rozdíl od předchozího bodu toto není něco co by se v dalších verzích dalo jednoduše změnit - jedná se o rozhodnutí při designu celého testovacího systému. Změna by znamenala zásadní změny v celém systému a pochopitelně i v již vytvořených unit testech. K čemuž se ale Oracle pravděpodobně neodhodlá.

UPDATE (2010/10/28): Pokud přemýšlíte nad tím proč byly použity právě anonymní bloky (přinášející různé nevýhody) a nikoliv kompilované bloky (jako v případě utPLSQL), je třeba si uvědomit že celé řešení je založeno na dvou samostatných DB spojeních - první je repositář ve kterém jsou uloženy definice unit testů, druhé spojení reprezentuje aplikační schéma na kterém jsou testy vyhodnocovány.

Pointa je v tom že se může naprosto nesouvisející schémat (dokonce úplně jiné instance), takže pokud by nebyly unit testy založeny na anonymních blocích musel by se unit vždy vytvořit a zkompilovat v aplikačním schématu, spusti se, oddebugovat se a po skončení zase zrušit (aby se ve schématu nehromadil bordel).

Příkladem opačného přístupu je již zmíněné utPLSQL - tam je jasně dáno že obě schémata (repository i aplikační) jsou v rámci jedné instance, není problém test zkompilovat přímo ve schématu repositáře, atd. Ale to má zase jiné nevýhody - například nelze jednoduše spouštět testy na různých schématech (s různými sadami dat, s různými větvemi zdrojáků, apod.) - jednou z možností jak toto vyřešit jsou synonyma.

Navázání na jednotlivé funkce/procedury

Další "vrozenou" vadou celého řešení je pevné navázání unit testů na procedury a funkce - každý unit test spouští (a testuje) právě jednu funkci nebo proceduru. Chcete otestovat něco složitějšího (například výsledek spuštění dvou procedur v určitém pořadí)? Smůla, nelze.

Důsledkem je že cokoliv chcete otestovat (např. proces skládající se z volání několika procedur a funkcí) musíte zapouzdřit do samostatné metody, pro kterou následně můžete vytvořit unit test. Což je zaprvé otrava, zadruhé vám díky tomu ve zdrojácích začnou bobtnat kusy kódu které jsou tam jen a pouze kvůli unit testům (testovaná procedura je uložena u dalších zdrojových kódů, nikoliv v repositáři).

Opět - je velmi nepravděpodobné že by se toto v dalších verzích mohlo změnit - je to prostě vlastnost.

Navázání na konkrétní schéma

Nejen že unit testy jsou navázány na konkrétní funkce, ony jsou navázány na konkrétní schéma.

navázání na schema

V čem je problém? Například pokud pro vývoj používáte několik různých schémat (např. pro každého vývojáře samostatné schéma, nebo schéma pro vývoj a schéma pro automatické testy, případně několik schémat s různými sadami dat), nebude možné unit testy mezi nimi jednoduše přenášet. Jediný způsob jak toto změnit je export do XML, ruční úprava XML a import do schématu. A to není nic pro slabší povahy ...

Problémy s přejmenováním testovaných metod

Obdobné problémy jako v případě schémat přichází i v případě jmen testovaných metod - jestliže metodu přejmenujete, test pochopitelně začne padat protože mu náhle chybí procedura kterou by měl volat. Můžete buď vytvořit nový test a překopírovat do něj kód z původního testu, nebo použít obdobný trik jako v případě schémat - úpravu exportovaného XML.

A toto neplatí jen pro názvy metod ale i pro jejich argumenty, protože testovací framework používá jmennou notaci. Takže například změníte název parametru z parametr_A na parametr_B a máte smůlu.

Poznámka: Toto by snad mělo být řešeno novým příkazem "synchronize" v některé následující verzi SQL Developeru.

Nemožnost indikace problémů v testech

Pokud jste zvyklí že při změně která něco "rozbíjí" vám IDE automaticky ukáže které kusy kódu se rozbily (např. zneplatní závislé package v důsledku přejmenování metody kterou volají), máte smůlu. Jediná šance jak zjistit jestli test funguje je spustit ho - i to je důsledek založení na anonymních blocích, stejně jako nemožnost debuggingu (a i v tomto případě je možnost opravy v dalších verzích velmi velmi nepravděpodobná).

Nutnost spouštění přes SQL Developer

Další drobná komplikace je důsledkem toho že celé vyhodnocování unit testů je realizováno v rámci SQL Developeru. Alternativou by mohla být například PL/SQL package (jako v případě utPLSQL), nicméně provázání na SQL Developer je daleko hlubší - unit testy jsou spojeny s konkrétními connections takže je nelze spustit jinak než přes SQL Developer.

V SQL Developeru existuje utilita ututil.bat která umožňuje spouštění unit testů z příkazové řádky (a také export a import), ale to není nic jiného než SQL Developer spouštěný bez GUI. Pro automatické buildy a testy to použitelné je, nicméně znamená to že na serveru musíte mít nainstalovaný SQL Developer a v něm správné connections.

Navíc je tento proces poměrně časově náročný - pro nahrání každého XML souboru (je jedno jestli obsahuje suite nebo samostaný test) nebo spuštění testů je nutno spustit JVM a SQL Developer, provést danou operaci (import / spuštění), a potom zase JVM ukončit. A to může být časově poměrně náročné - na našem build serveru jedno spuštění ututil.bat trvá cca 15 - 20 vteřin, a když se to vynásobí tak jeden build trvá klidně 40 minut.

Toto by šlo v dalších verzích částečně zlepšit umožněním "batchových operací" - aktuálně je možné pracovat vždy jen s jednotlivými elementy (suite, test, XML soubor), a pro každou operaci je nutno znovu spouštět instanci SQL Developeru což přináší brutální overhead. Pokud by bylo možno zadat "naimportuj tyto soubory" resp. "spusť tento seznam testů" byl by overhead minimalizován.

Nemožnost popisu unit testů

Drobnost, ale zamrzí - v aktuálním frameworku není u unit testů žádné místo vyhrazené pro dokumentaci nebo popis unit testu. Teoreticky to lze vložit např. do startup sekce, ale to sebou nese určité problémy. Zaprvé můžete mít startup sekci realizovanou sdíleným kódem (přes "library"), zadruhé to prostě není sekce určená k popisu unit testu. Stejně tak není možné si nechat vypsat unit testy s popisem.

Poznámka: Toto by snad mělo být v některé následující verzi SQL Developeru.

Jiné chování při běhu z GUI a při běhu z příkazové řádky

Nejzákeřnější problém jsem si nechal na konec. Spočívá v tom že celý nástroj se chová trochu jinak když testy spouštíte z GUI (při psaní testů), a když ho spouštíte z příkazové řádky (většinou v rámci automatického buildu). V GUI všechno funguje správně, ale automatický build náhle začne z nějakého důvodu padat - důvodem je trochu jiné chování v případě commitu / rollbacku.

Malá ukázka - vytvořte si "dummy" proceduru TEST_PROCEDURE

create or replace
PROCEDURE
TEST_PROCEDURE AS BEGIN NULL; END TEST_PROCEDURE;

a pracovní tabulku na které budeme problém demonstrovat problémy

CREATE TABLE TEST_TABLE ( test_col INTEGER PRIMARY KEY );

Nyní vytvořte unit test TEST_PROCEDURE_UNIT_TEST se startupem

BEGIN
  INSERT INTO TEST_TABLE (test_col) VALUES (1);
END;

a teardownem

BEGIN
  ROLLBACK;
END;

tj. test připraví data do tabulky TEST_TABLE, spustí proceduru a data zase odstraní. Pokud test spustíte z GUI, bude fungovat dle očekávání (i při opakovaném spuštění). Zkuste ho ale spustit z příkazové řádky, a dostanete přes prsty

UtUtil.bat -run -test -name TEST_PROCEDURE_UNIT_TEST -repo utrepos \
           -db devel -log 3
94791a76-ee94-4b2b-9729-81610f744853
UT_ERROR
TEST_PROCEDURE_UNIT_TEST failed: Startup User PL/Sql Code failed: \
ORA-00001: unique constraint violated (DEVEL_SCHEMA.SYS_C0036121)
ORA-06512: na line 2

Důvodem je že SQL Developer kamsi před spuštěním teardownu vloží commit, tj. rollback v teardownu data z tabulky neodstraní a dojde k výjimce.

Celý problém jde ještě dále komplikovat využitím test suites (tam se dokonce neprojevuje pokud jste pro repository i testovanou proceduru použili stejnou connection) a nebo pomocí savepointů, ale pro ilustraci to myslím stačí.

Mně osobně tato chyba připravila několik velmi dlouhých nocí kdy jsem se snažil odhalit chyby v kódu a v unit testech, než jsem zjistil že to je vlastně chyba přímo v testovacím frameworku.

Konkrétní příčinu problému neznám, nicméně troufám si tipovat na nějaký problém s autonomními trasakcemi (nástroj musí používat autonomní transakce pro logování zápis výsledků testů, jinak by se odrolovaly v případě zavolání rollbacku v některé části testu). Někde pravděpodobně chybí PRAGMA AUTONOMOUS_TRANSACTION nebo tak něco. Ale proč se to chová jinak z GUI a jinak z příkazové řádky, to mi jasné není ...

Komentáře

K tomuto článku zatím žádné komentáře neexistují (nebo čekají na schválení).

Nový komentář

Všechny komentáře podléhají schválení - mezi odesláním komentáře a jeho zobrazením na této stránce tedy může být prodleva. Vyplníte-li e-mailovou adresu, budete o schválení či neschválení komentáře informováni.

V titulku ani v textu nejsou povoleny HTML tagy - budou automaticky odstraněny. Odstavec ukončíte prázdným řádkem.

(nepovinné)