Dienstag, 14. Juli 2015

Row-Level-Security mit Oracle Virtual Private Database

Virtual Private Database (VPD) ist ein Security Feature der Oracle Database Enterprise Edition (eingeführt mit Version 8i, teilweise auch unter der Bezeichnung Fine Grained Access Control - FGAC bekannt). Es bietet eine sehr einfache und verlässliche Möglichkeit zur Umsetzung von Row-Level-Security Konzepten, d.h. der Einschränkung der Sichtbarkeit (und Änderbarkeit) auf Ebene einzelner Datensätze.

Beispiel Bestellsystem

Nehmen wir als Beispiel ein Bestellsystem in welchem Kunden Aufträge platzieren können. Üblicherweise wird sich in der Datenbank eines solchen Systems eine Tabelle ORDERS mit allen Aufträgen aller Kunden wiederfinden. Jeder ORDER Datensatz enthält eine CUSTOMER_ID als Foreign Key auf den Kunden der den Auftrag platziert hat.

   +-------------+               +-------------+
   | ORDERS      |               | CUSTOMERS   |
   +-------------+               +-------------+
   | ORDER_ID    |---------------| CUSTOMER_ID |
   | CUSTOMER_ID | *           1 | NAME        |
   | ...         |               | ...         |
   +-------------+               +-------------+


In der zugehörigen Anwendung soll ein konkreter Anwender i.d.R. immer nur Zugriff auf seine eigenen Aufträge haben. Egal ob die Anwendung über einen technischen Nutzer oder mit einem personalisierten Account des konkreten Anwenders auf die Datenbank zugreift, sie kann (die notwendigen SELECT Rechte vorausgesetzt) zunächst einmal alle Datensätze aus der Tabelle ORDERS auslesen. 

SQL> SELECT * FROM ORDERS;

 ORDER_ID CUSTOMER_ID ORDER_DATE
---------- ----------- ------------------
100001         123 13-MAR-15
100002         234 17-MAR-15
100003         123 21-MAR-15
100004         345 25-MAR-15
...

Es liegt oft in der Verantwortung der Anwendung nur die Datensätze des aktuell angemeldeten Anwenders abzufragen und anzuzeigen.

SQL> SELECT * FROM ORDERS WHERE CUSTOMER_ID = 123;

 ORDER_ID CUSTOMER_ID ORDER_DATE
---------- ----------- ------------------
100001         123 13-MAR-15
100003         123 21-MAR-15

Verlagerung der Security Regeln in die Datenbank

Mit VPD kann diese Verantwortung in die Datenbank verlagert werden. Vereinfacht ausgedrückt kümmert sich VPD darum, dass jede Query welche auf die geschützte Tabelle zugreift automatisch um die oben genannte WHERE-Bedingung ergänzt wird und das transparent und in jedem Fall. 

Die Verlagerung der Security Regeln in die Datenbank ist insbesondere dann interessant, wenn über verschiedene Wege (z.B. Applikation, Service-Schnittstellen und evtl. sogar direkt per SQL Client) auf die Daten zugegriffen wird. Security Regeln werden auf diesem Weg einmalig und zentral definiert/hinterlegt und in allen Fällen angewendet. Aber auch wenn ausschließlich eine Anwendung auf die Datenbank zugreift kann die Verlagerung in die Datenbank Sinn machen um den eher querschnittlichen Security Aspekt nicht in jedem einzelnen SQL-Statement der Anwendung berücksichtigen zu müssen.

Aufbau und Arbeitsweise einer VPD

Nachfolgend beschreibe ich die wesentlichen Komponenten einer VPD sowie deren Bedeutung und Zusammenspiel anhand des konkreten Beispiels von oben. Um die beschriebenen Schritte ausführen zu können benötigt der verwendete DB User neben den üblichen Privilegien (CREATE SESSION, CREATE TABLE, CREATE PROCEDURE, usw.) die folgenden speziellen Privilegien:

CREATE ANY CONTEXT
EXECUTE ON DBMS_SESSION
EXECUTE ON DBMS_RLS

Mit VPD ist es prinzipiell möglich alle Zugriffe (SELECT, INSERT, UPDATE und DELETE) auf einzelne Datensätze, und sogar auf einzelne Spalten, einzuschränken. Im  weiteren Verlauf betrachte ich jedoch nur das Lesen (SELECT) von vollständigen Datensätzen. Für weiterführende Informationen sei auf die unten aufgeführten Quellen verwiesen.

Security (Policy) Function

Die Security Policy Function (kurz auch Security Function) ist eine herkömmliche PL/SQL Function (auch Package Function möglich) welche die WHERE-Bedingung (Predicate) erzeugt die von VPD automatisch an die Queries gehängt wird. Sie definiert die Anwendungsspezifische Logik nach der die Security-Einschränkungen ermittelt werden. Die Function muss zwei Input-Parameter für den Schema- sowie den Objekt-Namen des Objects (Table, View, Synonym) erwarten und als Rückgabewert die WHERE-Bedingung zurückliefern, die beim Zugriff auf das Objekt verwendet werden soll. 

CREATE OR REPLACE FUNCTION orders_sec_fnc(
            p_schema IN VARCHAR2, 
            p_object IN VARCHAR2) RETURN VARCHAR2
IS
BEGIN
 RETURN 'CUSTOMER_ID = ' || ;
END orders_sec_function;
/

Eine Security Function kann für mehrere Objekte verwendet werden, wenn die WHERE-Bedingungen identisch oder zumindest ähnlich sind. Es können jedoch auch individuelle Security Functions für jedes zu schützende Objekt definiert werden.

Application Context

Häufig greifen Security Functions auf einen Application Context zu um daraus die notwendigen individuellen Informationen für die WHERE-Bedingung herauszulesen. In unserem Fall zum Beispiel die CUSTOMER_ID des zugreifenden Anwenders. Ein Application Context muss explizit angelegt werden und ist mit einem Package verbunden welches die API zum Ablegen und Auslesen von Inhalten bereitstellt.

CREATE OR REPLACE CONTEXT orders_sec_ctx 
            USING orders_sec_ctx_pkg;
In meinen bisherigen Projekten erfolgt die Identifizierung  und Authentifizierung des Anwenders außerhalb der Datenbank in der Applikation. Daher muss diese die notwendigen Informationen ggf. vor der Ausführung einer Query in den Application Context der Datenbank schreiben. Als API dient das Application Context Package.

CREATE OR REPLACE PACKAGE orders_sec_ctx_pkg IS 
 PROCEDURE set_customer_id(p_customer_id IN NUMBER);
END;
/
CREATE OR REPLACE PACKAGE BODY orders_sec_ctx_pkg IS
 PROCEDURE set_customer_id(p_customer_id IN NUMBER)
 AS
 BEGIN
            DBMS_SESSION.SET_CONTEXT('orders_sec_ctx', 
                'customer_id', p_customer_id);
 END set_customer_id;
END;
/

Die Security Function kann die Customer ID des aktuellen Anwenders nun aus dem Application Context auslesen.

CREATE OR REPLACE FUNCTION orders_sec_fnc(
            p_schema IN VARCHAR2, 
            p_object IN VARCHAR2) RETURN VARCHAR2
IS
BEGIN
          RETURN 'CUSTOMER_ID = SYS_CONTEXT(''orders_sec_ctx'', 
                                      ''customer_id'')';
END orders_sec_fnc;
/

Security Policy

Die Security Policy verbindet die Security Function mit einem zu schützenden Objekt (Tabelle, View, Synonym). Sie wird mit Hilfe des Packages DBMS_RLS angelegt. 
Die Parameter Object Schema und Name definieren dabei das zu schützende Objekt, in unserem Fall die ORDERS Tabelle.
Über den Parameter Policy Name gebe ich der Policy einen sprechenden Namen.
Die Parameter Function Schema und Policy Function definieren die zu verwendende Security Policy Function.
Der Parameter Statement Types definiert, dass die Policy ausschließlich für SELECT Statements angewendet werden soll.

BEGIN
 DBMS_RLS.ADD_POLICY (
   object_schema    => 'vpd_owner', 
   object_name      => 'orders', 
   policy_name      => 'orders_sec_policy', 
   function_schema  => 'vpd_owner',
   policy_function  => 'orders_sec_fnc', 
   statement_types  => 'select');
END;
/

VPD in Aktion

Mit dem Anlegen der Security Policy ist VPD für diese Tabelle "scharf geschaltet". Das wird unmittelbar sichtbar wenn ich auf die ORDERS Tabelle zugreife.

SELECT * FROM ORDERS;
no rows selected

Standardmäßig sehe ich erst einmal gar nichts mehr. Erst wenn ich eine gültige Customer ID in den Application Context schreibe kann ich danach die zugehörigen Ergebnisse sehen.

BEGIN
 orders_sec_ctx_pkg.set_customer_id(123);
END;
/
SELECT * FROM ORDERS;
 ORDER_ID CUSTOMER_ID ORDER_DATE
---------- ----------- ------------------
100001         123 13-MAR-15
100003         123 21-MAR-15

Links

Montag, 22. September 2014

Jersey 2 und JAXB - Deutliche Performance-Steigerung durch Wechsel von EclipseLink MOXy auf Jackson

In der vergangenen Woche haben wir ein Performance-Problem in einem unserer REST Services analysiert. Der Service dient einer Reihe von internen Applikationen als zentrales Gateway zu einem externen Provider von Patentinformationen. Auf Anfrage ruft er bei dem externen Dienstleister Patentinformationen in Form von JSON Objekten ab um diese dann in ein internes XML-Format umzuwandeln und an die aufrufenden Applikationen zurückzugeben.
Sowohl die Server-Seite für die internen Applikationen, als auch die Client-Seite zur Anbindung des externen Dienstleisters ist mit Jersey 2 realisiert. Für die Umwandlung werden die vom Dienstleister abgerufenen JSON-Strings mit Hilfe von JAXB in ein Objektmodell überführt. Dieses wird anschließend in ein Zielmodell transformiert und wiederum über JAXB in das auszuliefernde XML-Format serialisiert. Als JAXB-Implementierung verwendeten wir bisher EclipseLink MOXy, die Standard-Implementierung bei Jersey.

Das Problem

Die grundsätzliche Verarbeitungsgeschwindigkeit unseres Services war in Ordnung. Er war in der Lage ein bis zwei Patent-Dokumente pro Sekunde zu verarbeiten, was uns nicht in Jubel verfallen lies, für unsere angebundenen Applikationen jedoch vermutlich ausreichend war. Während der abschließenden Tests fiel uns allerdings auf, dass einige der Anfragen z.T. mehrere Sekunden dauerten. Bei steigender Last stieg auch die Häufigkeit dieser lang laufenden Anfragen, sowie die Zeit die diese benötigten. In Ausnahmefällen konnten wir Antwortzeiten von 45 Minuten (!) und mehr beobachten. Außerdem war auffällig, dass die CPU-Nutzung während dieser lang laufenden Anfragen kontinuierlich hoch war. Bei Batch-Abfragen mit mehreren parallelen Threads ließ sich anhand der CPU-Auslastung deutlich erkennen, wieviele Threads gerade mit einer solchen lang laufenden Anfrage beschäftigt waren.

Die Suche nach der Ursache

Zunächst vermuteten wir die Ursache beim externen Dienstleister, dessen Service auch an anderen Stellen bereits einige Schwächen gezeigt hatte. Wir analysierten das System mit Hilfe von VisualVM und fanden heraus, dass die lang laufenden Prozesse immer an derselben Stelle hingen, nämlich in der javax.ws.rs.core.Response bei der Ausführung der Methode readEntity(Class entityType). Das schien unseren Verdacht zunächst zu bestätigen. Wenn das Auslesen des Inhalts der Response lange dauert, liegt das vermutlich daran, dass dieser vom Server zu langsam geliefert wird. Zwei Punkte gab es jedoch, die uns komisch vorkamen:

  1. Wir hatten in Jersey eine Timeout-Zeit von 90 Sek. konfiguriert, deren prinzipielles Funktionieren wir auch beobachten konnten. Warum griff diese in den von uns beobachteten Fällen nicht?
  2. Wenn unser Service lediglich auf die Übertragung der Antwortdaten vom Dienstleister wartet, woher kommt dann die hohe CPU-Auslastung?

Ein genauerer Blick in die "hängenden" Threads führte uns dann auf die richtige Spur. Die Threads warteten nicht sondern waren eifrig damit beschäftigt die z.T. recht großen JSON-Strings vom Dienstleister (bis zu 30 MB!) zu parsen. Knackpunkt war dabei eine Methode des org.eclipse.persistence.internal.oxm.record.json.JSONReaders (EclipseLink MOXy):


Neben der etwas gewöhnungsbedürftigen Signatur fiel uns vor allem der interne Umgang mit der übergebenen Zeichenkette auf. Zum einen wird der Ergebnis String per einfacher String-Konketenation (+=) zusammengebaut, wofür in Java extra effizientere Methoden (StringBuffer bzw. StringBuilder) vorhanden sind. Zum anderen arbeitet die Methode an einigen Stellen mit Substrings. 
Über die geänderte Implementierung der Substring-Methode ab der Java-Version 7 waren wir bereits an einer anderen Stelle gestolpert. Im Gegensatz zur Java 6 Implementierung, bei der ein Substring weiterhin auf das selbe Character-Array im Speicher verwiesen und lediglich das relevante Fenster (Offset, Count) neu definiert hat, legt die Java 7 Implementierung eine Kopie des Character-Arrays im Speicher an (siehe dazu auch http://www.programcreek.com/2013/09/the-substring-method-in-jdk-6-and-jdk-7/). 
Diese geänderte Vorgehensweise macht an vielen Stellen Sinn (wenn aus einem großen Strings nur einige wenige Zeichen zur weiteren Verarbeitung benötigt werden und der Rest verworfen werden kann). In unserem speziellen Fall führte sie jedoch dazu, dass die ohnehin schon speicherintensiven Strings noch mehrere Male kopiert wurden.

Der Wechsel zu Jackson

Auf der Suche nach Lösungen kamen wir zu der Idee, dass der Austausch einer JAXB Implementierung in der Theorie doch eigentlich problemlos möglich sein sollte. Schließlich definiert das JDK die verwendete API und ich muss lediglich die Implementierung meiner Wahl hinzufügen. In früheren Projekten hatten wir recht gute Erfahrungen mit Jackson gesammelt. Wir entschieden uns es einfach zu versuchen, auch wenn wir nicht sicher waren, ob eine andere Implementierung unser Problem wirklich beheben würde. 

Im ersten Ansatz suchten wir uns die zu unserer Jersey Version 2.5.1 passende Jackson Bridge jersey-media-json-jackson 2.5.1 heraus. Leider mussten wir beim Aktualisieren der Dependencies feststellen, dass diese noch auf die Jackson Version 1.9.13 verwies. Eine Jackson 2.x Version hatten wir uns allerdings schon vorgestellt. Ein Versuch der vorhandenen Jersey Version einfach ein Jackson 2 unterzujubeln funktionierte erwartungsgemäß nicht. Also galt es auch Jersey zu aktualisieren. 

Die jersey-media-json-jackson Bridge verwendet Jackson 2 ab der Version 2.9. Da war der Weg bis zur aktuellen Version Jersey Version 2.11 auch nicht mehr weit. Wir aktualisierten Jersey 2.5.1 also auf die Version 2.11, entfernten die jersey-media-moxy Bridge sowie EclipseLink MOXy selbst und fügten statt dessen die jersey-media-json-jackson Bridge und die aktuelle Jackson Version 2.4.1 hinzu. Neben den von der Jersey-Bridge referenzierten Jackson-Bibliotheken fügten wir außerdem noch die jackson-dataformat-xml Bibliothek hinzu, um auch die Serialisierung ins interne XML-Format über Jackson abbilden zu können. Nach dem Update der Dependencies galt es dann ein paar Compiler-Fehler zu beheben.

Das bisher im Jersey Client registrierte MoxyJsonFeature wurde durch das JacksonFeature ersetzt. Anstelle eines custom MoxyJsonContextResolver erstellten wir einen analogen JacksonObjectMapperContextResolver (nach Vorbild der Jersey Dokumentation) und registrierten ihn ebenfalls im Jersey Client.

In einigen Beans unseres internen Objektmodells hatten wir einzelne Properties mit der MOXy-Annotation @XmlCDATA versehen um für diese bei der Serialisierung nach XML die Kapselung in einer CDATA-Sektion zu erzwingen. Dies ist notwendig, da die enthaltenen Textdaten HTML-Tags zur Formatierung enthalten, die für die Anzeige in den angebundenen Applikationen verwendet werden und somit nicht maskiert werden sollen. In Jackson gibt es keine Möglichkeit eine CDATA-Kapselung gezielt für einzelne Properties zu erzwingen. Als Alternativlösung erstellten wir einen CDataContentHandler (gefunden bei stackoverflow), der für alle serialisierten Properties angewendet wird und die Werte in eine CDATA-Sektion kapselt wenn es notwendig ist. Den Content Handler geben wir dem JAXB Marshaller in der marshal-Methode mit.

In dem Bean Package für das Objektmodell des Dienstleister-Formats hatten wir in einer jaxb.properties Datei noch die MOXy JAXBContextFactory registriert. Welche Context Factory hier für Jackson einzutragen ist haben wir nicht herausgefunden, also haben wir die Datei kurzer Hand gelöscht. Die anschließenden grünen Integrationstests haben uns recht gegeben. Der Umstieg war geschafft. Fairerweise muss gesagt werden, dass wir uns die hier aufgeführten Punkte mühsam zusammensuchen mussten und somit insgesamt gut einen Tag mit dem Umstieg zugebracht haben. Nicht zuletzt war das auch der Grund das alles in einem Blog-Artikel festzuhalten.

Das Resultat

Nachdem alle Tests wieder grün waren blieb die spannende Frage: Was hat die ganze Aktion jetzt gebracht.

Wie deployten den umgestellten REST Service in unsere Testumgebung, ließen die ersten kleinen Lasttests dagegen laufen und beobachteten gebannt die CPU- und Memory-Auslastung in VisualVM bzw. der JConsole. Die Verbesserung war enorm, viel größer als wir sie erwartet hatten. Hatte unser Service zuvor unter Last im Durchschnitt zwischen 500 und 800 MB Speicher und zwischen 40 und 60 % CPU-Last (mit Peeks bis zu 90 %) verursacht, so kommt er jetzt mit um die 250 - 300 MB Speicher und halbwegs konstant unter 10 % CPU-Last aus. Das spürt man auch an den Antwortzeiten und der Stabilität. Lang laufende Anfragen kommen nicht mehr vor. 

Fazit: Die umfangreiche Analyse sowie die Umstellung auf Jackson haben sich für uns auf jeden Fall gelohnt. Für zukünftige Projekte werden wir vermutlich gleich auf Jackson setzen.

Mittwoch, 19. Juni 2013

Zum Beispiel ein Test

Beispiele als Grundlage für Anforderungsanalyse, Spezifikation und Implementierung



Einleitung


Beispiele helfen in der Analysephase die Kundenanforderungen zu verstehen und zu diskutieren. Später können diese dann als Basis für automatisierte Akzeptanztests verwendet werden. Klingt ganz einfach? In der Praxis kann es jedoch einige Hürden geben, insbesondere wenn es um die Erweiterung einer existierenden Anwendung geht. Im Vortrag werden Erfahrungen aus einem Kundenprojekt geschildert und unterschiedliche Möglichkeiten aufgezeigt um aus definierten Beispielen automatisierte Akzeptanztests abzuleiten.


Analyse und Spezifikation von Anforderungen


In dem zugrundeliegenden Kundenprojekt wurde ein bestehendes Softwaresystem um eine Reihe von neuen Features erweitert. Grundlage für die Entwicklung jedes einzelnen Features war die Aufnahme und Analyse der Kundenanforderungen und deren Überführung in eine Spezifikation des zu entwickelnden Features. Dazu wurden Anforderungsworkshops durchgeführt, in denen der Auftraggeber seine Anforderungen präsentierte und mit dem Analysten diskutierte. Die Aufgabe des Analysten bestand darin die Ergebnisse der Diskussionen in eine „Prosa“-Spezifikation zu überführen. Diese wurde anschließend vom Auftraggeber geprüft, freigegeben und schließlich zur Umsetzung an den Entwickler übergeben.

Das Ergebnis dieser Vorgehensweise war häufig ernüchternd. Das fertige Feature entsprach nur zum Teil den Erwartungen des Kunden. Missverständnisse und Spezifikationslücken wurden oft erst im Rahmen der Abnahmetests des Kunden aufgedeckt.

Ursache: Auftraggeber, Analyst und Entwickler haben die Spezifikation jeweils nach ihren eigenen Vorstellungen interpretiert und sie „so verstanden, wie sie sie verstehen wollten“. Jeder hat nach bestem Wissen und Gewissen das spezifiziert, geprüft und entwickelt was er verstanden hat. Ein gemeinsames Verständnis fehlte, es fiel über diesen Weg jedoch nicht auf.


Beispiele


Beispiele veranschaulichen und verdeutlichen Zusammenhänge und helfen dadurch Missverständnisse und Lücken aufzudecken. Im Gegensatz zu einer allgemeingültig formulierten “Prosa”-Spezifikation verlangen Beispiele konkrete Angaben.

Nachfolgend exemplarisch eine Spezifikation sowie ein dazu formuliertes Beispiel um dies zu verdeutlichen:

Spezifikation:
Die Afa einer Investition ergibt sich 
aus dem Investitionswert, 
linear verteilt über den Abschreibungszeitraum, 
beginnend mit dem Monat der Aktivierung

Beispiel:
Wenn wir eine Investition im Wert von 600 TEUR getätigt haben, 
die am 01.01.2012 aktiviert wurde, 
dann folgt daraus, 
bei einer Abschreibungsdauer von 60 Monaten, 
eine monatliche AfA in Höhe von 10 TEUR 
und zwar von Jan/2012 bis Dez/2016

Die Verwendung von konkreten Werten führt oft dazu, dass Fachbegriffe verdeutlicht und weitere Details aufgedeckt werden. So war in der vorliegenden Spezifikation zunächst nur von einem  „Abschreibungszeitraum“ die Rede. Dass es eine definierte „Abschreibungsdauer von 60 Monaten“ gibt wurde erst durch das Beispiel offensichtlich. Außerdem werfen konkrete Beispiele häufig weitere Fragen auf, wie: „Erfolgt die Aktivierung immer am Anfang eines Monats?“ oder „Ist auch eine andere Abschreibungsdauer als 60 Monate möglich?“

Im dem zugrundeliegenden Kundenprojekt wurden in Anforderungsworkshops bis dahin nur vereinzelt Beispiele verwendet um schwierige Sachverhalte zu erklären oder um als Diskussionsgrundlage zu dienen. In den meisten Fällen wurden sie jedoch spätestens nach der Überführung in die Spezifikation verworfen. Dabei können solche Beispiele im Laufe der Diskussion wachsen und weitere Details aufdecken. Angefangen von einem grundlegenden Standardfall werden schnell Spezialfälle identifiziert und können wiederum durch entsprechende Beispiele festgehalten werden. Schnell entsteht auf diesem Weg eine Sammlung von Beispielen, die ein Feature nachvollziehbar und überprüfbar beschreibt. Beispiele, die vom Kunden oder mit dem Kunden zusammen erarbeitet werden geben auch Aufschluss darüber, was dem Kunden wichtig ist und was er vom System erwartet. Damit werden neben den Details eines Features auch die „Akzeptanzkriterien“ deutlich die der Kunde an das System stellt.

Noch wichtiger als die resultierenden Beispiele ist die Diskussion die diese auslösen. Sie führt dazu dass die Teilnehmer das Feature wesentlich intensiver beleuchten und detaillierter verstehen als dies sonst der Fall wäre.

Allerdings sind Beispiele nicht immer so einfach aufzustellen, wie es auf den ersten Blick aussieht. Es verlangt einige Übung innerhalb einer Diskussion die richtigen Beispiele zu identifizieren und sich auf die relevanten Inhalte zu beschränken. Oft existieren umfangreiche Einflussfaktoren die zu einem konkreten Ergebnis führen. Es kann mühsam sein, all diese Einflussfaktoren zu identifizieren und mit geeigneten Werten zu belegen. Das Ziel sollte daher immer sein sich auf das Wesentliche zu beschränken.

Zusammenfassend lässt sich folgende Aussage festhalten:

Durch die Verwendung von Beispielen 
wird die Spezifikation eines Features nicht einfacher 
sondern besser.

Sollen bestehende Features eines Systems geändert werden enthalten die entsprechenden Beispiele oft umfangreiche Teile des vorhandenen Systems. Werden diese innerhalb eines Anforderungsworkshops „nachspezifiziert“ entstehen häufig Aussagen wie „Das haben wir doch alles schon mal besprochen. Alles soll bleiben wie bisher, nur…“ Es hat sich in diesen Fällen als hilfreich erwiesen bereits im Vorfeld aus dem bestehenden System grundlegende Beispiele abzuleiten und diese im Workshop dem Kunden als Ausgangspunkt für die Spezifikation der Änderungen zu präsentieren.


Specification by Example


In seinem Buch „Bridging the Communication Gap“ beschreibt Gojko Adzic unter dem Begriff „Specification by Example“ die Verwendung von Beispielen als alleinige Spezifikation. Alle relevanten Aspekte eines Features werden dabei vollständig in Form von Beispielen beschrieben. Eine Prosa-Spezifikation ist demnach nicht mehr notwendig und entfällt.

Führt man diesen Ansatz konsequent weiter können alle definierten Beispiele in Summe als die Kriterien festgehalten werden bei deren Erfüllung das entwickelte Feature als akzeptiert (= abgenommen) gilt. Die Entwicklung erfolgt dann von Anfang an basierend auf diesen „Akzeptanzkriterien“ und der Entwickler hat sehr konkrete Vorgaben was das neue Feature leisten muss. Man spricht in diesem Fall von „Acceptance-Test Driven Development“  (ATDD).

In vielen Fällen ist der Kunde jedoch eine Prosa-Spezifikation gewohnt und sieht sie oft sogar explizit in seinem Vorgehensmodell und entsprechenden Dokumentvorlagen als Artefakt vor. Darüber hinaus lassen sich nicht alle Sachverhalte in Form von Beispielen beschreiben. Insbesondere Zusammenhänge zwischen einzelnen Sachverhalten sowie Begründungen für das geforderte Verhalten können damit nicht ausreichend beschrieben werden. Insofern wurde im konkreten Projekt ein Mittelweg eingeschlagen bei dem die Prosa-Spezifikation umfangreich durch entsprechende Beispiele ergänzt wurde. Im Prosa-Teil werden dabei neben der Problemstellung, den Rahmenbedingungen und dem Lösungsentwurf i.W. die Kern-Features sowie die wichtigsten Sonderfälle beschrieben. Die Beispiele verdeutlichen die Prosa-Beschreibung und ergänzen weitere Sonder- und Randfälle.


Vom Beispiel zum Test


Neben den bereits beschriebenen Eigenschaften von Beispielen bieten diese einen weiteren Vorteil:

Beispiele können nachgestellt bzw. ausgeführt werden

Damit sind die Beispiele aus der Analyse/Spezifikation eine optimale Grundlage zur Definition von Akzeptanztests.

Formuliert man das zuvor genannte Beispiel etwas um wird dies deutlich:

Wir haben eine Investition im Wert von 600 TEUR
aktiviert am 01.01.2012
und eine Abschreibungsdauer von 60 Monaten

Ausgangssituation/-zustand
Wenn wir die Afa berechnen

Aktion/Ereignis
dann folgt daraus
eine monatliche AfA
in Höhe von 10 TEUR
in der Zeit von Jan/2012 bis Dez/2016
Erwartetes Ergebnis

Damit können die definierten Beispiele eines Features als „Definition of Done“ für die Entwicklung festgelegt werden. Wenn alle definierten Beispiele (= Akzeptanztests) erfolgreich durchlaufen werden ist das Feature vollständig umgesetzt.

Es ist nicht zwingend erforderlich diese Akzeptanztests automatisiert umzusetzen. Sie können auch manuell ausgeführt werden. Spätestens wenn zum betreffenden Feature weitere Änderungsanforderungen vom Kunden gemeldet werden zahlt sich eine Automatisierung jedoch aus. Darüber hinaus können automatisierte Akzeptanztests regelmäßig ausgeführt werden um sich gegen Regressionsfehler zu schützen.

Im Falle einer Java-Anwendung können die automatisierten Akzeptanztests entweder mit Hilfe von Spezifikationsframeworks wie z.B. JBehave, Cucumber oder Concordion oder ausschließlich auf Basis von JUnit umgesetzt werden. In allen Fällen sollten die Tests wie eine Spezifikation lesbar sein Im Optimalfall entsteht dadurch eine „Lebenden Spezifikation“, d.h. eine Beschreibung der vollständigen Funktionalität einer Anwendung in Form von automatisierten Akzeptanztests.

Donnerstag, 8. November 2012

Configurable entities with fluent interface

When testing Java applications you usually have to create entities with some kind of test data. Beside the content you need for your tests you often have to define some more stuff, just to get a valid entity. Particularly, in an integration test scenario, where you want to store your test entity in a database, you may additionally have to create some master or referenced entities to be able to save your data. Doing this in every single test (class) will pollute your tests very soon and makes it hard to read and maintain them.

In my last projects I used "configurable" entity extensions in combination with factory classes that create fully configured default entities to get my test code clean and readable. The API of the configurable entities mainly provides a "fluent interface" as described some years ago by Martin Fowler and Eric Evans (see http://martinfowler.com/bliki/FluentInterface.html).

Let's have a look at an example to illustrate the idea.

Imagine you have a Customer entity like the following.


public class Customer {

    private String firstName;
    private String lastName;
    private String eMail;
    private String phoneNumber;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    ...
}

If you just use its Java Bean API to create a new customer in your test setup you would have to write something like this:

@Test
public void doSomethingWithACustomer() {
    Customer aCustomer = new Customer();
    aCustomer.setFirstName("Donald");
    aCustomer.setLastName("Duck");
    aCustomer.setEMail("donald@ent.net");
    aCustomer.setPhoneNumber("001-1234-98765");
    ...
}


A ConfigurableCustomer that I usually would write looks as follows:


public class ConfigurableCustomer extends Customer {

    public ConfigurableCustomer withFirstName(String firstName) {
        this.setFirstName(firstName);
        return this;
    }

    public ConfigurableCustomer withLastName(String lastName) {
        this.setLastName(lastName);
        return this;
    }

    public ConfigurableCustomer withEMail(String eMail) {
        this.setEMail(eMail);
        return this;
    }

    public ConfigurableCustomer withPhoneNumber(String phoneNumber) {
        this.setPhoneNumber(phoneNumber);
        return this;
    }
}

It extends the regular Customer and provides one with-method for each property of the entity. Each of these methods just does two things:
  • it calls the regulary setter for this property to set the given value
  • it returns the ConfigurableCustomer instance itself
With this test extension, we can refactor the formerly written test:

@Test
public void doSomethingWithACustomer() {
    ConfigurableCustomer aCustomer = new ConfigurableCustomer()
        .withFirstName("Donald")
        .withLastName("Duck")
        .withEMail("donald@ent.net")
        .withPhoneNumber("001-1234-98765");
    ...
}

This code is a little slighter and there's less duplication in it. But that's not the most important change. It is also much more readable. It can almost be read like a part of a specification.

Now, what if all we need to configure in our test is the E-Mail address of our customer? Of course, we  need to define all the other data somewhere to get a valid customer to work with. But in our test setup we only want to describe the parts that are necessary for the test.

That is when I usually introduce a CustomerFactory in my test sources.


public class CustomerFactory {

    /**
     * Creates a ConfigurableCustomer using some default data.
     * @return the Customer.
     */
    public static ConfigurableCustomer create() {
        return new ConfigurableCustomer()
                .withFirstName("Donald")
                .withLastName("Duck")
                .withEMail("donald@ent.net")
                .withPhoneNumber("001-1234-98765");
    }
}

It provides one - or sometimes more - methods to create a customer fully configured with some default data. It returns a ConfigurableCustomer instance, so we can easily overwrite individual properties as needed. If we only need an arbitrary customer, not interrested in its details, we can just take the default entity as it is. Therefore, the factory always should return a fully filled and valid entity.

In our test, where we just need a customer with a known email, we can now use the following code:


@Test
public void doSomethingWithACustomer() {
    Customer aCustomer = CustomerFactory.create()
                .withEMail("a.known@email.com");
    ...
}

We will get a valid customer named "Donald Duck" with the default phone number and the defined email. All done with a tiny and readable piece of code in our test.

If we later on have to modify the Customer entity, e.g. add some more properties, all we have to do is to also extend the CustomerFactory to set reasonable default values for the new fields. By extending the ConfigurableCustomer class, too, we also make this new fields configurable.

That's it. No magic, no rocket science, just some simple patterns that make your testing life easier. Feel free to use it in your code and let me know your experiences with it.

Remark: If you're using JPA or another object-relational mapping technology you may experience some limitations extending your entity classes. In this case we have to find another way to implement our ConfigurableCustomer, e.g. let it wrap the Customer entity instead of extending it. Doing this, we no longer can easily use the ConfigurableCustomer everywhere a "normal" Customer is expected. Instead, we have to call a toCustomer() method to get the wrapped entity.


public class ConfigurableCustomer {

    private Customer customer;

    public ConfigurableCustomer withFirstName(String firstName) {
        customer.setFirstName(firstName);
        return this;
    }

    public ConfigurableCustomer withLastName(String lastName) {
        customer.setLastName(lastName);
        return this;
    }

    ...

    public Customer toCustomer() {
        return customer;
    }
}


Our test case using this wrapping ConfigurableCustomer would look like this.


@Test
public void doSomethingWithACustomer() {
    Customer aCustomer = CustomerFactory.create()
                .withEMail("a.known@email.com")
                .toCustomer();
    ...
}

That's a few more characters to type and a little cumbersome to read, but still much better than our example at the beginning of this post.


(Thanks to @stefanscheidt for review!)

Dienstag, 9. Oktober 2012

Build grails-app on travis-ci.org

I've just successfully configured a continous integration build of my new grails web application using travis-ci.org.

Travis-ci.org is a continuous integration service for the open source community. It is free to use and fully integrated with github. It supports multiple programming languages including java and groovy.

To get started with travis-ci.org log in using your github account and read the getting started tutorial.

Once you have activated your github repository you need to add a .travis.yml file to your repository containing the necessary build configuration settings for travis-ci to build your application.

For a grails application you can use the following .travis.yml configuration:

language: groovy

jdk:
- oraclejdk7

before_install:
- sudo add-apt-repository -y ppa:groovy-dev/grails
- sudo apt-get update
- sudo apt-get install grails-2.1.1


script: grails test-app

Thanks to berngp@github for publishing gist!

Mittwoch, 16. Mai 2012

Simple Java DateBuilder

I took some time on a trip to Munich to extract a little DateBuilder class from one of my latest Java projects and publish it to github. It's no big thing, just a simple wrapper around the default GregorianCalendar with a nice and simple interface. Check it out and feel free to use it in your projects.


Usage:

// two days ago from today
DateBuilder.today().daysAgo(2);

// 15-09-2012
DateBuilder.givenDate(15, 5, 2012).monthsAhead(4);

// first day of next month
DateBuilder.today().day(1).monthAhead(1);

// or
DateBuilder.today().firstDay().nextMonth();

Freitag, 27. Mai 2011

Testen von erwarteten Exceptions mit JUnit4

Beim durcharbeiten einer Schulung zum Thema "Microtesting" werde ich gerade auf ein Feature in JUnit4 aufmerksam, welches ich bisher noch nicht kannte.

Wollte ich bisher testen, dass eine Methode eine Exception auslöst, so habe ich in etwa folgenden Test-Code geschrieben:

@Test
public void methodCallShouldThrowNumberFormatException() {
  try {
    objectUnterTest.methodUnderTest();
    fail("Should throw NumberFormatException");
  } catch ( NumberFormatException exc ) {
    // everything is fine
  }
}

Das ist zum einen etwas umständlich, zum anderen kann es je nach verwendeten Code-Analyse Einstellungen auch zu Warnungen führen, dass der Catch-Block nichts tut.

Schöner und deutlich einfacher ist die Verwendung des Attributes "expected" der @Test Annotation.

@Test ( expected = NumberFormatException.class )
public void methodCallShouldThrowNumberFormatException() {
  objectUnterTest.methodUnderTest();
}

Damit teile ich JUnit mit, dass ich bei der Durchführung dieses Tests erwarte dass eine NumberFormatException ausgelöst wird. Ist dies der Fall bewertet JUnit den Test als erfolgreich (grün). Fliegt keine oder eine andere Exception schlägt der Test fehl.

Als weiteres Attribut der @Test Annotation kann eine "timeout" Zeitspanne angegeben werden, nach welcher der Test mit einem Fehler abgebrochen wird. Sicherlich auch nicht falsch, das zu wissen.