05. Nov 2013
Einbindung von PL/SQL Code in den Continuous Integration Prozess
Einleitung
Dies ist der versprochene zweite Teil der Artikelserie, welche ich im März 2013 begonnen hatte. Im ersten Teil ging es im wesentlichen um die Einbindung von DDL und DML Skripte in den CI-Prozess. In diesem Artikel stelle ich anhand einer Oracle Datenbank vor, wie sich analog zu JUnit-Tests PL/SQL Code im Rahmen der CI automatisiert testen lässt.
In vielen Java-Projekten erlebe ich immer wieder, dass der Bereich Datenbank bzgl. Tests eher stiefmütterlich behandelt wird. Bei einer guten Projektinfrastruktur findet man zwar eine CI Umgebung (bamboo/jenkins) vor, in der der aktuelle Entwicklungsstand automatisiert “vertestet” wird und den Entwicklern als Fangnetz dient, jedoch deckt diese meist nur den Java-Teil der Applikation ab. In vielen Fällen wird zudem auf das DB-Backend in der CI Umgebung ganz verzichtet, dabei bedeutet CI doch gerade contineous integration. Der Verzicht hat häufig den Grund, dass man die Einbindung des DB-Backends eher als langläufige Integrationstests ansieht und man lieber schneller Feedback von der CI Umgebung haben möchte, aber auch dafür gibt es Lösungen (z.B. mit einer H2 in memory DB, die den gewünschten DB-Dialekt emuliert).
Teil II: Automatisiertes Testen von PL/SQL Code
Vorab noch mal ein paar grundsätzliche Überlegungen. Während bei der Java-Entwicklung Code häufig von mehreren Entwicklern bearbeitet wird, und es in der Java-Welt daher umso wichtiger ist schnell Feedback über entsprechende JUnit-Tests zu bekommen, wird in der Datenbank-Entwicklung nicht selten der Code von einer Person betreut. Dies erkennt man häufig auch schon am Projektsetup, z.B. bei fünf Java Entwickler zu einem DB-Entwickler. Man könnte also auch argumentieren, die Investition in die CI Einbindung lohnt sich hier nicht, da es sowieso ein Experte ist, der den Code kontrolliert und sich somit weniger Fehler einschleichen können. Diese ist meiner Erfahrung nach eine zu kurzsichtige Auffassung. Was ist, wenn der Kollege in der letzten Projektphase ungeplant wegfällt (Urlaub, Lottogewinn etc..) und der Code übernommen werden muss, oder das Projekt in einer späteren Wartungsphase von einem ganz anderen Team betreut wird? Es lohnt sich bei der Entscheidung für ein solches Vorgehen eher mittelfristig bis langfristig zu denken. Durch das Bereitstellen von Tests für die Datenbank-Entwicklung gewinnt man:
- das frühzeitige Erkennen von Fehlern und damit
- mehr Qualität in der Entwicklung
- Regressionstest-Fähigkeit des Backend-Codes in der Wartungsphase
- und die Möglichkeit für den DB-Entwickler Test-Driven vorzugehen (TDD)
Wahl der Waffen
Beschäftigt man sich mit der Recherche nach entsprechender Testunterstützung für PL/SQL-Entwickler so landet man in der Regel bei folgenden Möglichkeiten:
Links: [1] = http://www.oracle.com / [2] = http://www.quest.com / [3] = http://dbunit.sourceforge.net/ / [4] = / [5] = http://utplsql.sourceforge.net/
Obige Aufzählung erhebt kein Anspruch auf Vollständigkeit, im Rahmen
des Artikels belasse ich es jedoch bei aufgeführten Auswahl.
Unter der Prämisse, dass ich das gesamte Software-Projekt in einer
CI-Umgebung wie Jenkins oder Bamboo sie zur Verfügung stellen möchte,
fällt die Wahl durch ein Ausschlussverfahren auf utPLSQL, da:
- Die UI gestützten Systeme die Einbindung in die Testautomatisierung nicht unterstützen.
- DBUnit vom Datenbank Entwickler erfordern würde, zusätzlich etwas Java und eine Java-IDE zu erlernen. (Allerdings, würde ich diesem Framework den Vorzug geben, wenn die DB-Engineers über die Zusatzqualifikation verfügen).
- PLUnit letztlich nur ein SubSet von utPLSQL darstellt, das Projekt brach liegt, und man hier nicht mit Unterstützung rechnen kann.
Setup von utPLSQL
Nach dem Download und Entpacken der aktuellen Version (22) von der Projekt-Seite folgt man zunächst der Anleitung und erzeugt einen eigenen DB-Benutzter der die Framework-Tabellen aufnimmt:
connect system/manager create user utp identified by utp default tablespace users temporary tablespace temp; grant create session, create table, create procedure, create sequence, create view, create public synonym, drop public synonym to utp; alter user utp quota unlimited on users;
[stextbox id="info"]
benötigte UTL_FILE Konfiguration
Da die Packages des utPLSQL Framework UTL_FILE verwenden, um ggf.
Testpakages einzulesen und vor der Ausführung erneut zu kompilieren,
benötigt der User utp die entsprechenden Ausführungsrechte. Dies kann
der DBA sicherstellen (init.ora benötigt die Angabe eines utl_file_dir
und als sys: grant execute on utl_file to utp).
[/stextbox]
Anschließend verbindet man sich mit dem eben erzeugten User (utp) und führt das Installationsscripts des Frameworks aus:
@ut_i_do install
Damit ist utPLSQL auch schon einsatzbereit. Als nächstes bauen wir unser Beispiel Szenario auf:
Beispiel-Testszenario
Nun zu einem konkreten Testbeispiel: Eine Kundendatenbank in einer heterogenen Anwendungslandschaft, d.h. die Datenbank wird von verschiedenen Applikationen verschiedener Technologien gemeinsam genutzt. Ein Teil der Businesslogik des Kundensystems wurde im Persistenz-Layer in Form von PL/SQL Code abgebildet. Da Datenqualität für den Kunden enorm wichtig ist, wird z.B. auch die Prüfung ob ein Kundendatum eine gültige E-Mail-Adresse enthält durch das Backend abgesichert. Anwendungen, welche es versäumen die E-Mail-Adresse bei der Eingabe korrekt zu validieren sollen dies mit einer DB-Exception quittiert bekommen (Der Einfachheit halber prüfen wir für dieses Beispiel nur auf das Vorhandensein des Klammeraffen @).
Die nachfolgenden Skripte sorgen für die entsprechende Infrastruktur unseres Beispiels:
Anlegen des Applikations Users
connect system/Manager create user crm identified by crm default tablespace users temporary tablespace temp; grant create session, create table, create procedure, create sequence, create view, create public synonym, drop public synonym to crm; alter user crm quota unlimited on users; GRANT EXECUTE ANY PROCEDURE TO "CRM" ;
Da ich es bevorzuge, das Framework in einen eigenen Schema zu installieren, benötigt der User des Schemas, in dem sich die Unit Tests und die zu testenden Module befinden Ausführungsrechte. In Ermangelung der Möglichkeit any grants auf ein Schema zu vergeben, vergebe ich hier any grants auf die gesamte DB. Wer das nicht möchte, muss diese einzeln vergeben, oder das utPLSQL Framework direkt in das zu testende „Schema" injizieren.
PL/SQL Beispiel
create or replace PROCEDURE ADD_CUSTOMER_PRC ( P_NAME IN VARCHAR2 , P_EMAIL IN VARCHAR2 ) AS BEGIN -- demo check logic IF (instr(p_email, '@') < 1) THEN raise_application_error(-20000, 'no valid email adress'); END IF; -- Perform some real business logic END ADD_CUSTOMER_PRC;
Testausführung mittels utPLSQL
Als nächstes schreibt der DB-Entwickler das Äquivalent zu einer JUnit-Testklasse, nur mit dem Unterschied, dass er im gewohnten PL/SQL Code bleibt. Das Package legen wir im gleichen Schema ab, wo auch das zu testende Objekt residiert – hier also im crm-Schema:
Package Body der Testklasse
CREATE OR REPLACE PACKAGE BODY UT_ADD_CONSUMER_PRC AS PROCEDURE ut_setup AS BEGIN /* TODO Implementierung */ NULL; END ut_setup; PROCEDURE ut_teardown AS BEGIN /* TODO Implementierung */ NULL; END ut_teardown; PROCEDURE ut_add_customer_validation AS v_result NUMBER := 0; BEGIN -- Invalid Email should lead to exception BEGIN add_customer_prc(p_name => 'Mustermann', p_email => 'No.Valid.Mail.Adress'); EXCEPTION WHEN OTHERS THEN v_result := 1; END; UTP.utassert.eq('Expected Exception',v_result,1); END ut_add_customer_validation; END UT_ADD_CONSUMER_PRC;
Schön zu sehen ist, dass das Framework hier analog zu JUnit set-up
und tear-down Callbacks anbietet. Die API verfügt ebenfalls über
umfangreiche asserts, für den Artikel greifen wir auf einen einzigen
equal zurück.
[stextbox id="info"]
Achtung Fallstrick
Das utPLSQL Framework hängt bei der Testausführung zwar ein „ut_"
prefix dran, prüft aber, ob es das Objekt ohne Präfix im data dictionary
gibt. Das bedeutet die Testklasse muss (bis auf dem Präfix) genauso
heißen, wie das zu testende Objekt.
[/stextbox]
So vorbereitet können wir nun aus dem crm-Schema heraus den Testfall aufrufen:
Testaufruf
exec utp.utplsql.test('ADD_CUSTOMER_PRC', recompile_in => FALSE);
Folgender Screenshot zeigt den erfolgreichen Beispiellauf (mit set serveroutput on):
Einbindung der utPLSQL Tests in die CI
Der letzte Schritt besteht nun in der Automatisierung. Hierzu bedienen wir uns dem utPLSQL Maven Plugin, welches in der Lage ist die utPLSQL Tests auszuführen. Folgende pom.xml zeigt das hierzu nötige setup:
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>utPLSQL_Example</groupId> <artifactId>utPLSQL_Example</artifactId> <version>1.0-SNAPSHOT</version> <name>Sample Maven utplsq Test Setup</name> <properties> <oracle.version>11.2.0</oracle.version> <db.host>localhost</db.host> <db.instance>XE</db.instance> <db.username>crm</db.username> <db.password>crm</db.password> </properties> <build> <plugins> <!-- TODO: Install Test-Packages with sql-maven-plugin --> <plugin> <groupId>com.theserverlabs.maven.utplsql</groupId> <artifactId>maven-utplsql-plugin</artifactId> <version>1.31</version> <dependencies> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc6</artifactId> <version>${oracle.version}</version> </dependency> </dependencies> <executions> <execution> <id>run-plsql-test-packages</id> <phase>test</phase> <goals> <goal>execute</goal> </goals> </execution> </executions> <configuration> <driver>oracle.jdbc.driver.OracleDriver</driver> <url>jdbc:oracle:thin:@${db.host}:1521:${db.instance}</url> <username>${db.username}</username> <password>${db.password}</password> <packages> <!-- Anmelden von Testpackages --> <param>ADD_CUSTOMER_PRC</param> </packages> </configuration> </plugin> </plugins> </build> </project>
[stextbox id="info"]
Installation der benötigten Maven-Artefakte
Das Maven-Artefakt für utPLSQL gibt es derzeit nicht auf den öffentlichen Repository-Servern. Zur Installation ins lokale repository lädt man es von der Plugin-Homepage http://code.google.com/p/maven-utplsql-plugin/downloads/list herunter und installiert es z.B. wie folgt ins lokale Repository:
mvn install:install-file -Dfile=maven-utplsql-plugin-1.31.jar -DpomFile=maven-utplsql-plugin-1.31-pom.xml
Ähnlich wird mit dem ojdbc verfahren. Hierzu stellt man das Artefakt einer lokalen oracleXE Installation wie folgt dem lokalen Repository zur Verfügung:
<em>C:devoraclexeapporacleproduct11.2.0serverjdbclib>mvn install:install-file -Dfile=ojdbc6.jar -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.2.0 -Dpackaging=jar</em>
[/stextbox]
Anschließend lassen sich die Tests über maven ausführen, hier ein Ausschnitt aus dem Testlauf:
mvn test
------------------------------------ TESTS ------------------------------------ utplsql:Testing package ADD_CUSTOMER_PRC Successes: 1, Failures: 0 Results: Tests run: 1, Failures: 0 [INFO] ADD_CUSTOMER_PRC, failed=0, passed=1, total=1 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.142s [INFO] Finished at: Sun Oct 06 21:35:30 CEST 2013 [INFO] Final Memory: 5M/120M [INFO] ------------------------------------------------------------------------
Damit haben wir alles was wir brauchen, um die Tests über die CI Umgebung (jenkins/bamboo) automatisiert auszuführen.
In einem vollständigen Setup würden wir die PL/SQL Artefakte über den maven build aus SVN bzw. GIT ziehen, in der CI Datenbank kompileren und die Tests ausführen. Somit ist gewährleistet, dass auch immer der aktuelle Entwicklungsstand gestestet wird.
Fazit
Über diese Art und Weise lässt sich mit vertretbaren Aufwand die PL/SQL Entwicklung in den CI Prozess integrieren. Etwas schade an utPLSQL ist, dass das ursprüngliche User-Forum nicht mehr existiert. Aus Sicht des reinen DB-Entwicklers bietet es jedoch ausreichend Möglichkeiten entsprechende Tests für eine Automatisierung bereitzustellen. Verfügen die DB-Entwickler zusätzlich über Java-KnowHow, würde ich persönlich den Weg über DBUnit einschlagen, da das Handling bzgl. Schreiben der Testklassen hier etwas weniger Overhead hat und man nicht so sehr mit dem Rechtesetup innerhalb der Datenbank „kämpfen" muss.
Damit endet meine kleine DB-CI Reihe. Ich hoffe, der ein oder andere konnte etwas mitnehmen und ich wünsche viel Erfolg und Spaß im Einsatz der Techniken.