Tester-Tea-Time (Teil 2): Rechtschreibung in Anwendungen – mehr als nur Kleinlichkeit

Die „Tester-Tea-Time“ ist ein Beitragsformat auf diesem Blog, in dem Themen aufgegriffen werden, die Testerinnen und Tester tagtäglich beschäftigen. Gewisse Problemstellungen oder Themen kehren immer wieder, daher soll hier eine Basis geschaffen werden, solche Phänomene zu erläutern und Lösungen dafür zu finden. Zudem sollen Diskussionen und neue Denkweisen angeregt werden. Im Testing können wir viel voneinander lernen, indem wir unseren Alltag beobachten!

Moderator: Willkommen zur Tester-Tea-Time! Im Interview mit Testerinnen und Testern der ZEISS Digital Innovation (ZDI) werden wir erneut spannende Themen diskutieren.

Widmen wir uns nun dem heutigen Thema. Dazu sprechen wir mit Sandra Wolf (SW), Testerin bei ZDI. Warum beschäftigen wir uns dieses Mal mit dem Thema „Rechtschreibung“ und wo siehst du den Zusammenhang mit der Softwareentwicklung?

SW: Der Alltag eines Testers hält viele Herausforderungen bereit. Gerade während des Testprozesses ist große Konzentration gefragt, wenn jedes Detail auf Qualität überprüft werden muss. Eines dieser Details ist die Rechtschreibung und Grammatik. Oft wird es unterschätzt, wie wichtig die korrekte Orthografie sein kann.
Im Arbeitsalltag passiert es oft, dass Rechtschreibfehler in der Software gefunden werden. Doch wenn diese zur Behebung an die Entwicklung gemeldet werden, kommt es nicht selten vor, dass der Tester dafür belächelt wird. Die vorherrschende Meinung ist, dass das nur sehr kleine und unwichtige Fehler wären. Im heutigen Gespräch soll mit dieser Meinung aufgeräumt werden. Rechtschreibung und Zeichensetzung sind nicht gerade die beliebtesten Themen und werden häufig als sehr trocken empfunden. Dabei sind gerade diese Regeln, die wir seit der Schulzeit lernen, eine Orientierungshilfe für uns und unser Gehirn. Ein Wort, das richtig geschrieben wird, wird auch leichter gelesen, im Satz zu einer Aussage kombiniert und somit im Gehirn verarbeitet. Aufmerksame Leser – oder im Fall der Softwareentwicklung – Nutzer werden zwangsläufig über falsche Rechtschreibung in der Software stolpern. Es wurde sogar nachgewiesen, dass bestimmte Persönlichkeitstypen unterschiedlich emotional auf falsche Orthografie reagieren (vgl. Weiß, 2016). Somit können Fehler in diesem Bereich im Gegensatz zu ihrem trockenen Ruf Emotionen auslösen, die dann als Konsequenz den Umgang mit der Software beeinflussen.

Zwei Kollegen zum Interview über Videokonferenz verbunden
Abbildung: Stanislaw Traktovenko und Sandra Wolf in der virtuellen Tester-Tea-Time im Gespräch.

Moderator: Von welcher Art der Beeinflussung sprechen wir in diesem Fall?

SW: Korrekte Rechtschreibung strahlt zum Beispiel Seriosität aus. In Bewerbungen und amtlichen Anträgen wird eine fehlerfreie Rechtschreibung vorausgesetzt. Es ist sogar in Studien belegt worden, dass beispielsweise die Chancen auf Bewilligung eines Kreditantrags durch sprachliche Fehler vermindert werden (vgl. Weiß, 2016). Übertragen wir das nun auf die Software, die wir entwickeln, ist nur ein möglicher Schluss zu ziehen: Rechtschreibung ist essenziell für eine adäquate Nutzung und die Außenwirkung der Software beim Kunden. Und somit sollte dieses Thema innerhalb des Entwicklungsprozesses eindeutig ernster genommen werden und mehr Aufmerksamkeit bekommen als bisher.
Schauen wir uns den Alltag von Testern und Entwicklern an, dann wissen wir, dass die Funktionalität der Software im Mittelpunkt steht. Natürlich ist nachvollziehbar, dass ein kosmetisch wirkendes Thema wie die Rechtschreibung hinter aufwendig programmierten Anwendungsteilen zurücktritt. Das sollte alle Beteiligten des Prozesses allerdings nicht über die Wichtigkeit hinwegtäuschen. Denn ganz klar ist, dass der Erfolg eines Produktes und somit auch einer Anwendung von der sprachlichen Qualität beeinflusst werden kann. Der erste Eindruck beim Lesen eines Textes oder beim Nutzen einer Software lässt uns automatisch auch auf den Bildungsgrad der Schöpfer schließen (vgl. Frost, 2020). Somit kann durch eine fehlerhafte Rechtschreibung ein schlechtes Licht auf eine gute Software geworfen werden.

Moderator: Wie muss ich mir dieses “schlechte Licht”, in dem die Software dann steht, im Detail vorstellen?

SW: Durch eine schlechte Rechtschreibung kann das Vertrauen in die Qualität der Software verloren gehen und die Akzeptanz für die Anwendung sinken. Der Nutzer könnte davon ausgehen, dass generell wenig Wert auf Qualität gelegt wird, wenn schon mit der Rechtschreibung nachlässig umgegangen wird. Schließlich drückt eine korrekte Orthografie nicht nur Professionalität, sondern auch einen gewissen Respekt gegenüber dem Leser/Nutzer aus. Es konnte sogar festgestellt werden, dass eine Textqualität beeinflusst, ob jemand vom Interessenten zum Käufer wird. Wird das auf die Softwareentwicklung bezogen, kann es auf jeden Fall Budget einsparen, wenn von Anfang an auf die Rechtschreibung geachtet wird und die Meldung solcher Fehler ernst genommen wird (vgl. Frost, 2020).
Letztendlich präsentieren wir in unseren Projekten auch unser Unternehmen, weshalb das Thema der Orthografie weitreichendere Auswirkungen haben kann, als wir zunächst denken. Im besten Fall kann durch eine gute Rechtschreibung der Ruf unserer Softwareentwicklung verbessert werden bzw. erhalten bleiben. Dadurch können wiederum mehr Kunden und höhere Umsätze erzielt werden, weil die gleichbleibende Qualität unserer Softwareprodukte ein Argument für eine Zusammenarbeit mit der ZDI sein kann.

Moderator: Hier möchte ich gern anknüpfen und Stanislaw Traktovenko (ST) aus unserem Usability-Team zu Wort kommen lassen. Welche Bedeutung nimmt das Thema der Rechtschreibung aus deiner Sicht ein? Siehst du ebenfalls die Auswirkungen in dem von Sandra Wolf beschriebenen Maße?

ST: Aus meiner Sicht hat die Rechtschreibung einen Einfluss auf die Lesbarkeit und dadurch auf die Wahrnehmung der Informationen in der Anwendung. Wir ordnen das den Usability-Prinzipien Konsistenz und Sprache zu. Rechtschreibfehler haben also potenziell eine direkte Auswirkung auf die Usability einer Anwendung. Eine inkorrekte Orthografie stört zum Beispiel den Lesefluss und somit die Wahrnehmung der Software durch den Nutzer. Es entsteht ein negatives Gefühl und der Nutzer beschäftigt sich nicht mehr mit der Aufgabe, die er mit der Software eigentlich verfolgt hatte. Er wird durch eine falsche Rechtschreibung abgelenkt und das beeinflusst seine Effektivität und Effizienz. Auch wenn Rechtschreibung nur ein kleiner Bruchteil der Usability ist, kann sie somit größere Auswirkungen haben als gedacht, so wie Sandra bereits vorher erläutert hat.

Moderator: Vielen Dank, Sandra und Stanislaw für diese interessanten Einblicke. Die Auswirkungen sind tatsächlich weitreichender als erwartet, das ist sehr erstaunlich. Wir können somit zusammenfassen, dass das trocken wirkende Thema der Rechtschreibung in allen Softwareprojekten ernst genommen werden muss, um eine möglichst hohe Qualität zu liefern und sowohl die Produkte als auch uns als Unternehmen adäquat zu präsentieren. Das Thema der Rechtschreibung wirkt im ersten Moment vielleicht banal, hat aber im Endeffekt eine große Wirkung und somit Wichtigkeit für uns alle. Das Thema sollte daher unbedingt die Aufmerksamkeit bekommen, die es verdient hat.

In den folgenden Beiträgen werden wir weitere Problemstellungen aus dem Alltag von Testerinnen und Testern aufgreifen und besprechen, welche möglichen Lösungsansätze es dafür gibt.

GraalVM – Ein Blick auf die Performance

Die GraalVM ist nun seit reichlich zwei Jahren am Markt. Sie verspricht im Wesentlichen zwei Dinge: Bessere Laufzeiteigenschaften und die Integration mehrerer Programmiersprachen.

Dieser Blogbeitrag konzentriert sich auf die Performance. Dabei soll es aber nicht primär darum gehen, ob und wieviel ein spezielles Programm auf dem Graal-JDK schneller ausgeführt wird als auf einem herkömmlichen JDK. Die konkreten Messwerte und die relativen Vergleiche sind ohnehin nicht nur vom untersuchten Programm abhängig und wenig verallgemeinerbar, sondern auch nur Momentaufnahmen: Sowohl die GraalVM als auch beispielsweise das OpenJDK werden beständig weiterentwickelt, so dass sich auch die Messwerte stets ändern werden. Stattdessen beschäftigt sich der Blogbeitrag vor allem mit den Fragen: Warum sollte die GraalVM wesentlich performanter sein? Was macht sie anders als die herkömmlichen JDKs? Damit wird eine Abschätzung möglich, ob alle Programme performanter ausgeführt werden oder keine nennenswerte Steigerung zu erwarten ist oder ob die Performance-Steigerung nur in bestimmten Anwendungsfällen zu erwarten ist. Und letztendlich, ob das „herkömmliche“ Java demzufolge zu langsam ist…

Laptop verbunden mit LED Display, auf dem "Hello World" abgebildet wird.

Entwicklung der Compiler

Da die Performance eines Java-Programms wesentlich durch den Compiler bestimmt wird und auch hier die Kernfrage ist, was die GraalVM anders macht verschaffen wir uns zunächst einmal einen Überblick über Compiler.

In den Anfängen der Programmierung existierte zunächst noch gar kein Compiler – es wurde direkt der Maschinencode programmiert. Da dies unübersichtlich und wenig verständlich war, hat sich zeitnah der Assembler-Code entwickelt. Dabei handelt es sich jedoch im Wesentlichen um eine direkte Abbildung des Maschinencodes – nur dass statt Binär- oder Hexadezimal-Opcodes nun Buchstabenkürzel verwendet werden. Auch hier sprechen wir (zumindest im Scope dieses Blogbeitrags) noch nicht von einer Programmiersprache und einem Compiler.

Mit der Zeit wurde die Entwicklung immer komplizierterer Programme notwendig. Damit wurde der Assembler-Code zunehmend unpraktikabel. Daher wurden in den 1950er Jahren die ersten höheren Programmiersprachen entwickelt. Diese benötigten einen Compiler, der den Quelltext in Maschinencode übersetzt.

Dies war zunächst der klassische AOT-Compiler (AOT: Ahead Of Time). Der Quelltext wird analysiert (Syntaxanalyse), in eine interne Baumstruktur überführt (Syntaxbaum) und aus diesem wird Maschinencode generiert (Codegenerierung). Die entstehende Binärdatei kann nun direkt ausgeführt werden.

Alternativ zur AOT-Kompilierung kann ein Programm auch durch einen Interpreter ausgeführt werden. Hierbei wird der Quelltext eingelesen und zeilenweise durch den Interpreter umgesetzt. Die eigentlichen Operationen (z. B. Addition, Vergleich, Programmausgabe) führt dann der Interpreter aus.

Der AOT-Compiler hat gegenüber dem Interpreter den Vorteil, dass die Programme wesentlich schneller ausgeführt werden. Allerdings sind die erzeugten Binärdateien maschinenabhängig. Darüber hinaus hat der Interpreter bessere Fehleranalysemöglichkeiten, da er beispielsweise Zugriff auf Laufzeitinformationen hat.

Java, Interpreter und JIT-Compiler

Beim Entwurf der Programmiersprache Java war ein Ziel, dass sie architekturneutral und portabel ist. Aus diesem Grund wurde der Java-Quellcode von Anfang an in maschinenunabhängigen Bytecode übersetzt. Dieser konnte dann von einer Laufzeitumgebung, der JRE (Java Runtime Environment), interpretiert werden. Damit war der übersetzte Bytecode maschinenunabhängig. So konnten beispielsweise Applets ohne Anpassungen auf einem Windows-PC, einem Mac oder einer Unix-Workstation ausgeführt werden. Die JRE muss dazu – unabhängig vom Applet – vorab auf den Workstations installiert sein.

Diese Mischform – AOT bis zum Bytecode, dann Interpretation zur Laufzeit – ist übrigens keine Idee aus der Java-Welt: Bereits in den 1970er Jahren hat beispielsweise Pascal den p-Code genutzt. [1]

Als die Java-Technologie im Jahre 1995 veröffentlicht wurde [2], war diese Maschinenunabhängigkeit zunächst mit großen Performance-Einbußen verbunden. Viele der damals relevanten Programmiersprachen wie bspw. „C“ kompilieren ihren Quellcode direkt in Maschinencode (AOT). Dieser kann auf dem entsprechenden System nativ ausgeführt werden und ist somit wesentlich performanter als die Interpretation des Bytecodes. Zu jener Zeit hat sich in den Köpfen vieler IT-Fachkräfte die Grundeinstellung „Java ist langsam“ festgesetzt – damals zu Recht.

Nun ist aber ein weiteres Entwurfsziel der Programmiersprache Java die hohe Leistungsfähigkeit. Aus diesem Grund wurde im Jahr 1998 der JIT-Compiler (JIT: Just In Time) eingeführt [2]. Damit wurden die Performance-Einbußen durch die reine Interpretation stark reduziert.

Bei der JIT-Kompilierung wird der Bytecode beim Programmstart zunächst ebenfalls interpretiert. Allerdings wird dabei genau verfolgt, welche Programmteile wie oft ausgeführt werden. Die häufig ausgeführten Programmteile werden nun – zur Laufzeit – in Maschinencode übersetzt. Zukünftig werden diese Programmteile nicht mehr interpretiert, sondern es wird der native Maschinencode ausgeführt. Hier wird somit zunächst Ausführungszeit für die Kompilierung „investiert“, um bei jedem zukünftigen Aufruf dann Ausführungszeit einsparen zu können.

Die JIT-Kompilierung ist daher ein Mittelweg zwischen AOT-Kompilierung und Interpretation. Die Plattformunabhängigkeit bleibt erhalten, da der Maschinencode erst zur Laufzeit erzeugt wird. Und da die häufig genutzten Programmteile nach einer gewissen Warmlauf-Zeit als nativer Maschinencode ausgeführt werden, ist auch die Performance (dann) annähernd so gut wie bei der AOT-Kompilierung. Ganz allgemein gilt dabei: Je häufiger einzelne Programmteile ausgeführt werden, desto mehr können die anderen, interpretierten Programmteile bei der Performance-Betrachtung vernachlässigt werden. Und dies gilt vor allem für oft durchlaufene Schleifen oder langlaufende Server-Anwendungen, deren Methoden ständig aufgerufen werden.

Laufzeitoptimierungen

Mit den bisher betrachteten Mechanismen ist der JIT-Compiler zwar plattformunabhängig – kann sich aber an die Ausführungszeit des AOT-Compilers nur herantasten, sie jedoch nicht erreichen oder gar übertreffen. Aus diesem Grund war zu dem Zeitpunkt, als der JIT-Compiler in das JDK integriert wurde, noch keineswegs sicher, dass es für einen Siegeszug ausreichen wird.

Allerdings hat der JIT-Compiler einen großen Vorteil gegenüber dem AOT-Compiler: Er ist nicht nur auf die statische Quellcode-Analyse angewiesen, sondern er kann das Programm direkt zur Laufzeit beobachten. Da sich die allermeisten Programme in Abhängigkeit von Eingaben und/oder Umgebungszuständen unterschiedlich verhalten, kann der JIT-Compiler zur Laufzeit wesentlich zielgenauer optimieren.

Ein großer Pluspunkt sind dabei die spekulativen Optimierungen. Dabei werden Annahmen getroffen, welche in den meisten Fällen zutreffen. Damit das Programm trotzdem korrekt funktioniert, wird die Annahme mit einem sogenannten „Guard“ abgesichert. Beispielsweise geht die JVM davon aus, dass in einem produktiv ausgeführten Programm die Polymorphie so gut wie nicht genutzt wird. Natürlich ist die Polymorphie grundsätzlich sinnvoll, aber die praktischen Einsatzszenarien beschränken sich meist auf den Testbereich oder auf die Entkopplung einer Codebasis – üblicherweise zur Nutzung durch verschiedene Programme oder für zukünftige Erweiterbarkeit. Während der Laufzeit eines konkreten produktiven Programmes – und dies ist der Scope der JVM – wird die Polymorphie jedoch selten genutzt. Das Problem ist dabei, dass es beim Aufruf einer Interface-Methode relativ zeitaufwendig ist, für das vorhandene Objekt die passende Methoden-Implementierung herauszusuchen. Aus diesem Grund werden die Methodenaufrufe getrackt. Wird beispielsweise mehrmals die Methode „java.util.List.add(…)“ auf einem Objekt vom Typ „java.util.ArrayList“ aufgerufen, merkt sich die JVM dies. Bei den folgenden Methodenaufrufen „List::add“ wird darauf spekuliert, dass es sich wieder um eine ArrayList handelt. Zunächst wird mit einem Guard die Annahme abgesichert: Es wird geprüft, dass das Objekt tatsächlich vom Typ ArrayList ist. Üblicherweise trifft dies zu und die bereits mehrfach ermittelte Methode wird mittels der „gemerkten” Referenz einfach direkt aufgerufen.

Seit der Integration des JIT-Compilers in das JDK sind nun mittlerweile mehr als zwei Jahrzehnte vergangen. In dieser Zeit wurden sehr viele Laufzeitoptimierungen integriert. Die vorgestellte Polymorphie-Spekulation ist nur ein kleines Beispiel, welches verdeutlichen soll: Neben der Kompilierung von Maschinencode wurden sehr viele Optimierungen erdacht, welche in einer komplexen Sprache wie Java nur zur Laufzeit funktionieren. Wenn eine Instanz beispielsweise mittels Reflection erzeugt wird, ist es für einen AOT-Compiler sehr schwer bis unmöglich, den konkreten Typ zu bestimmen und die spekulative Optimierung umzusetzen. Der Geschwindigkeitsvorteil der aktuellen JIT-Compiler beruht demnach im Wesentlichen darauf, dass sie dem Programm bei der Ausführung zuschauen, Gewohnheiten erkennen und schließlich Abkürzungen einbauen können.

GraalVM

Die GraalVM ist ein JDK von Oracle, welches auf dem OpenJDK basiert. Sie bringt eine virtuelle Maschine sowie viele Entwickler-Tools mit – was soweit auch für die anderen JDKs gilt. Warum also erregt die GraalVM wesentlich mehr Aufmerksamkeit als die anderen JDKs?

Zunächst bringt die GraalVM einen GraalVM-Compiler mit, welcher in Java entwickelt wurde. Darüber hinaus soll auch die gesamte JVM in Java umgeschrieben werden. Im letzten Kapitel wurde dargelegt, dass die aktuellen JVMs vor allem deswegen sehr performant sind, weil jahrzehntelang verschiedenste Optimierungen ergänzt wurden, welche sich nun summieren. Diese Optimierungen sind vor allem Java-spezifisch. Sie werden zumeist von Leuten entwickelt, welche einen Java-Background besitzen. Wenn nun die Ausführungsumgebung nicht in C++ sondern in Java umgesetzt ist, sind folglich keine C++-Kenntnisse mehr notwendig, um Optimierungen beizusteuern. Die Entwickler-Community wird so mittel- bis langfristig auf eine breitere Basis gestellt.

Ein weiterer spannender Aspekt der GraalVM ist, dass sie nicht nur Java-basierte Sprachen unterstützt. Das “Truffle Language Implementation Framework” ist ein Ansatzpunkt für die Entwicklung eigener Sprachen (DSL). Der GraalVM-Compiler unterstützt mit dem Truffle-Framework entwickelte Sprachen, so dass diese auch in der GraalVM ausgeführt werden können und von allen Vorteilen entsprechend profitieren. Einige Sprachen wie JavaScript, Python oder Ruby werden dabei bereits von Haus aus seitens der GraalVM unterstützt. Da alle Truffle-Sprachen gleichzeitig und gemeinsam in der GraalVM ausgeführt werden können, wird hier auch von einer polyglotten VM gesprochen.

Darüber hinaus werden auch LLVM-basierte Sprachen unterstützt. Bei LLVM handelt es sich um ein Rahmenprojekt für optimierende Compiler [4][5]. Dabei werden nicht nur Compiler-Bestandteile und -Technologien für externe Compiler-Entwicklungen bereitgestellt, sondern es werden auch im LLVM-Projekt bereits Compiler für viele Programmiersprachen wie bspw. C/C++ oder Fortran angeboten. Die LLVM Runtime ist ein weiterer Bestandteil der GraalVM, mit welchem LLVM-basierte Sprachen aufbauend auf dem Truffle-Framework in der GraalVM ausgeführt werden können. Auf den polyglotten Aspekt soll hier aber nicht weiter eingegangen werden, denn er hätte seinen eigenen Blogbeitrag verdient.

GraalVM Native Image

Die für diesen Beitrag relevanteste Neuerung ist die sogenannte „Native-Image-Technologie“. Das native-image ist ein Entwickler-Tool der GraalVM. Es erzeugt aus Bytecode eine ausführbare Datei. Die Ziele sind eine bessere Performance und weniger Hauptspeichernutzung zur Laufzeit. Nun wurde aber bisher beschrieben, dass Java immer schneller geworden ist: Der JIT-Compiler übersetzt alle häufig ausgeführten (d. h. relevanten) Programmteile in nativen Maschinencode. Das Programm wird während der Ausführung beobachtet und es werden fortlaufend Laufzeitoptimierungen vorgenommen. Damit stellt sich also die Frage: Was kann denn hier noch um Größenordnungen verbessert werden?

Die Antwort ist verblüffend einfach: Die Startzeit. Auch mit JIT-Compilern wird der Bytecode zunächst interpretiert. Erstens ist der Programmstart meistens kein häufig ausgeführter Programmteil. Zweitens müssen diese Programmteile üblicherweise erst ein paarmal durchlaufen werden, damit der JIT-Compiler diese als lohnendes Übersetzungsziel erkennt. Mit den Laufzeitoptimierungen verhält es sich analog: Das Programm muss erst einmal zur Laufzeit beobachtet werden, damit die passenden Optimierungen erkannt und eingebaut werden können. An dieser Stelle kommt verschärfend hinzu, dass beim Start alle benötigten Objekte sowie deren Klassen inklusive der kompletten Vererbungshierarchie initialisiert werden müssen.

Da wir nun eine Vorstellung vom „Was“ haben, interessiert uns das „Wie“: Wie kann unser Programm dazu bewegt werden, schneller zu starten?

Bei der Erstellung des Native Image wird der Bytecode zunächst sehr umfangreich statisch analysiert. Unter anderem wird dabei geprüft, welche Code-Teile zur Laufzeit überhaupt ausgeführt werden können. Dies bezieht sich nicht nur auf die vom Nutzer bereitgestellten Klassen, sondern auf den gesamten Klassenpfad – also inklusive der von der JVM bereitgestellten Java-Klassenbibliotheken. Nur die ermittelten Quellcode-Fragmente werden in das Native Image aufgenommen. Somit wird an dieser Stelle zwar der Umfang stark reduziert – aber es wird auch eine „Closed world assumption“ aufgestellt: Sobald irgendetwas dynamisch zur Laufzeit geladen wird, steht das Native Image Tool vor einem Problem. Es erkennt nicht, dass auch diese Quellcode-Teile ausgeführt werden können und damit benötigt werden. Aus diesem Grund wird auf diese Weise nicht viel mehr als ein einfaches HelloWorld-Programm funktionieren. Deshalb kann und muss man bei der Erstellung des Native Image dem Tool noch Informationen mitgeben, was alles dynamisch aufgerufen werden kann.

Nach der statischen Analyse wird nun der erste Punkt umgesetzt, welcher die Startgeschwindigkeit erhöht: Da der JIT-Compiler mit der Interpretation starten würde, wird ein AOT-Compiler genutzt, um Maschinencode zu erstellen. Das erzeugte Native Image ist, wie der Name schon impliziert, nativ ausführbarer Maschinencode. Damit geht allerdings die Plattformunabhängigkeit verloren.

Zusätzlich zum nativ kompilierten Programm wird die sogenannte SubstrateVM in das Native Image aufgenommen. Hierbei handelt es sich um eine abgespeckte VM, welche nur die zur Ausführung des Native Image notwendigen Komponenten wie beispielsweise Thread Scheduling oder Garbage Collection enthält. Die SubstrateVM hat dabei auch Limitierungen, beispielsweise ist die Unterstützung eines Security Managers gar nicht vorgesehen.

Eine zusätzliche Steigerung der Startgeschwindigkeit wird erreicht, indem das Native Image bei der Erstellung bereits vorab initialisiert wird. Das Programm wird dazu nach dem Kompilieren soweit gestartet, bis die wesentlichen Initialisierungen erfolgt sind, aber noch kein Input von außen verarbeitet werden muss. Von diesem gestarteten Zustand wird ein Speicherabbild erstellt und in das Native Image gelegt.

An das „Was“ und das „Wie“ schließt sich nun noch ein eher kritisches „Warum“ an: Der AOT-Compiler ist schon ein halbes Jahrhundert bekannt und Java existiert mittlerweile auch seit einem Vierteljahrhundert. Vor allem in der Anfangszeit von Java wurden verschiedene AOT-Ansätze ausprobiert – welche sich jedoch nicht durchsetzen konnten. Warum sollte es jetzt anders sein? Warum wird ausgerechnet jetzt eine geringere Startzeit interessant, wenn sie doch mit Nachteilen verbunden ist? Warum sind die performanten Antwortzeiten im darauffolgenden tage- oder wochenlangen Betrieb plötzlich weniger wichtig?

Die Antwort ist im Cloud Computing zu suchen: Hier werden die Services in einer anderen Form zur Verfügung gestellt. Bisher wurden die Services vorwiegend in einem Anwendungscontainer betrieben, welcher Tag und Nacht ausgeführt wird und in welchem das Programm schon seit Tagen komplett durchoptimiert? ist. Schließlich wurde der Anwendungscontainer üblicherweise auch bei spärlicher Nutzung (z. B. Tageszeiten-abhängig) nicht heruntergefahren. Im Gegensatz dazu kann in der Cloud die Service-Infrastruktur bei Nicht-Nutzung problemlos heruntergefahren werden – somit können Kapazitäten gespart werden. Beim nächsten Aufruf wird die Infrastruktur wieder hochgefahren und der Aufruf wird ausgeführt. Das bedeutet, dass die Programme in der Cloud nicht im Dauerbetrieb laufen, sondern dass es sich unter Umständen bei jedem Aufruf um einen Kaltstart handelt. Demzufolge ist die Startzeit hier „auf einmal“ sehr entscheidend. Und da zu erwarten ist, dass zukünftig noch mehr Java-Programme in der Cloud statt in einem Anwendungscontainer ausgeführt werden, wird sich die Fokussierung auf die Startzeit wohl noch verstärken.

Hands On: HelloWorld

Nach der ganzen Theorie möchten wir dem JDK nun bei der Arbeit zuschauen. Dazu kommt zunächst die in Listing 1 dargestellte Klasse „HelloWorld“ zum Einsatz.

package de.zeiss.zdi.graal;

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Moin!");
    }

}

Listing 1

Zunächst die klassische Variante: Wir befinden uns auf einer Linux-VM und es ist ein OpenJDK installiert:

> java --version

openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)

java 11.0.12 2021-07-20 LTS
Java(TM) SE Runtime Environment GraalVM EE 21.2.0.1 (build 11.0.12+8-LTS-jvmci-21.2-b08)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 21.2.0.1 (build 11.0.12+8-LTS-jvmci-21.2-b08, mixed mode, sharing)

Mit diesem Setup kompilieren wir die HelloWorld-Klasse (javac) und führen den entstandenen Bytecode auf einer JVM aus:

time java -cp target/classes de.zeiss.zdi.graal.HelloWorld

Damit erhalten wir folgende Ausgabe:

Moin!

real    0m0.055s
user    0m0.059s
sys     0m0.010s

Für die Auswertung ist hier die Summe der beiden Zeilen „user“ und „sys“ relevant. Das ist die Rechenzeit, die für die Programmausführung notwendig war – in dem Fall also ca. 69ms.

Eine Anmerkung zu den 55ms: Das HelloWorld-Programm hat vom Start bis zu seiner Beendigung 55ms „Real-Zeit“ (die Zeit, welche der Nutzer wahrnimmt) benötigt. Das ist weniger als die 69ms benötigte Rechenzeit. Dies liegt daran, dass das eingesetzte Linux-System mehrere Prozessoren besitzt. Für unsere Messungen soll hier aber die vom System aufgewandte Rechenzeit betrachtet werden. Erstens ist die Rechenzeit unabhängiger davon, wie viele Prozessoren das Programm ausgeführt haben. Und zweitens ist dies in der Cloud beispielsweise auch die Zeit, welche der Anwendungsbetreiber bezahlen muss.

Nun sind wir auf die GraalVM gespannt. Auf deren Homepage [3] wird sie zum Download angeboten. Für unsere Evaluation ist die Enterprise-Variante („Free for evaluation and development“) passend, da ein Großteil der Performance-Optimierungen nur hier enthalten sind.

Die Installation ist für Linux sehr gut dokumentiert und funktioniert nahezu problemlos. Damit ist die GraalVM als JDK nutzbar.

> java --version

java version "11.0.12" 2021-07-20 LTS
Java(TM) SE Runtime Environment GraalVM EE 21.2.0.1 (build 11.0.12+8-LTS-jvmci-21.2-b08)
Java HotSpot(TM) 64-Bit Server VM GraalVM EE 21.2.0.1 (build 11.0.12+8-LTS-jvmci-21.2-b08, mixed mode, sharing)

Unser HelloWorld-Programm können wir nun genauso mit dem GraalJDK kompilieren (javac) und ausführen. Damit erhalten wir die folgende Ausgabe:

Moin!

real    0m0.084s
user    0m0.099s
sys     0m0.017s

Interessanterweise benötigt die JVM des GraalJDK nahezu 70 % mehr Rechenzeit, um unser HelloWorld-Beispiel als Bytecode auszuführen. Nun verspricht die GraalVM den signifikanten Performance-Vorteil allerdings auch nicht primär bei der Ausführung von Bytecode, sondern bei Nutzung der Native-Image-Technologie.

Das native-image (das Entwickler-Tool) ist in der heruntergeladenen GraalVM noch nicht enthalten, dafür existiert aber das Kommandozeilen-Tool „gu“ (GraalVM Updater). Mit diesem kann man zusätzliche Komponenten nachladen, verwalten und aktualisieren. Auch hier unterstützt die Dokumentation der GraalVM sehr gut. Mit dem nachgeladenen Entwickler-Tool können wir nun aus dem Bytecode das Native Image erzeugen. Im Fall eines so trivialen Programms wie unserem HelloWorld-Beispiel genügt dazu ein einfacher Kommandozeilenbefehl mit dem vollqualifizierten Klassennamen als Argument:

cd ~/dev/prj/graal-eval/target/classes
native-image de.zeiss.zdi.graal.HelloWorld

Die Erstellung des HelloWorld Native Image benötigt reichlich 3 Minuten Rechenzeit – und das ausführbare Programm ist ca. 12 MB groß. Auf den ersten Blick vergleicht man die Größe vermutlich mit dem Bytecode: Die HelloWorld.class ist lediglich 565 Byte groß. Allerdings enthält das Native Image nicht nur die kompilierte Klasse, sondern alle relevanten Teile der Java-Klassenbibliothek sowie die SubstrateVM. Verglichen mit der Größe einer JRE liegt das Native Image nur noch bei grob geschätzt 10 %.

Doch zurück zu unserem Native Image: Wir haben es erfolgreich erstellt, können es ausführen und erhalten dabei die folgende Ausgabe.

time ./de.zeiss.zdi.graal.helloworld
Moin!

real    0m0.004s
user    0m0.003s
sys     0m0.001s

Dieses Resultat können wir erst einmal als relevanten Geschwindigkeitsgewinn stehen lassen.

Hands On: HelloScript

Bei der GraalVM wird immer wieder herausgestellt, dass es sich dabei nicht nur um eine Java-VM sondern um eine polyglotte VM handelt. Aus diesem Grund erweitern wir unser HelloWorld-Programm noch um einen kleinen Exkurs in die JavaScript-Welt. Der Quellcode dazu ist in Listing 2 dargestellt. Der wesentliche Unterschied ist hier der notwendige Übergang von der Java- in die JavaScript-Welt.

package de.zeiss.zdi.graal;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class HelloScriptEngine {

    public static void main(String[] args) throws ScriptException {

        ScriptEngine jsEngine = new ScriptEngineManager().getEngineByName("javascript");

        System.out.print("Hello ");
        jsEngine.eval("print('JavaScript!')");
    }

}

Listing 2

Neben dieser universellen JavaScript-Anbindung mittels der javax.script.ScriptEngine wollen wir auch die Graal-spezifische JavaScript-Anbindung ausprobieren. Dazu nutzen wir org.graalvm.polyglot.Context. Der Quelltext ist in Listing 3 dargestellt.

package de.zeiss.zdi.graal;

import org.graalvm.polyglot.Context;

public class HelloScriptPolyglot {

    public static void main(String[] args) {

        System.out.print("Hello ");
        try (Context context = Context.create()) {
            context.eval("js", "print('JavaScript!')");
        }
    }

}

Listing 3

Die beiden HelloScript-Programme werden analog dem HelloWorld-Programm in Bytecode übersetzt. Bei der Erstellung der Native Images muss das Entwickler-Tool noch informiert werden, dass die JavaScript-Welt genutzt werden wird. Dies geschieht mit dem folgenden Aufruf:

cd ~/dev/prj/graal-eval/target/classes
native-image --language:js de.zeiss.zdi.graal.HelloScriptEngine
native-image --language:js de.zeiss.zdi.graal.HelloScriptPolyglot

Anschließend kann der Bytecode auf den VMs oder die Native Images nativ ausgeführt werden. Da das HelloScriptPolyglot Graal-spezifisch ist, können wir es allerdings nicht ohne Weiteres auf dem OpenJDK ausführen.

Ein Blick auf die Messwerte

Die drei Szenarien wurden jeweils als Bytecode auf dem OpenJDK, als Bytecode auf dem GraalJDK und als Native Image ausgeführt. Die durchschnittlichen Programmausführungszeiten sind in Tabelle 1 aufgeführt.

Hello WorldHelloScriptEngineHelloScriptPolyglot
Bytecode OpenJDK69 ms1321 msX
Bytecode GraalJDK116 ms2889 ms2775 ms
Native Image4 ms13 ms11 ms

Tabelle 1: Beispiel für durchschnittliche Programmausführungszeiten

Auf den ersten Blick fällt ins Auge, das die Ausführung als Native Image in allen drei Szenarien jeweils um ein Vielfaches schneller ist als die übliche Bytecode-Ausführung.

Auf den zweiten Blick fällt aber auch auf, dass die Bytecode-Ausführung mit dem GraalJDK wesentlich mehr Rechenzeit benötigt als mit dem OpenJDK: Im HelloWorld-Beispiel knapp 70 % mehr, bei dem HelloScriptEngine-Beispiel über 100 % mehr. Das wurde von Oracle so nicht kommuniziert, ist aber grundsätzlich auch kein allzu großes Problem, da die Motivation für den Einsatz der GraalVM vermutlich nicht in der schnelleren Bytecode-Ausführung liegt. Man sollte diesen Fakt aber im Hinterkopf behalten, wenn man den relevanten Speed-Up durch das Native Image ermitteln möchte: Zur Erstellung des Native Image muss schließlich die GraalVM installiert sein. Wenn man nun zum Vergleich die Bytecode-Ausführung misst und „java -jar …“ ausführt, wird der Bytecode mittels der GraalVM ausgeführt. Da im produktiven Betrieb aber bisher vermutlich eher das OpenJDK eingesetzt wurde, sollte man eher mit diesem vergleichen – und damit wäre der Speed-Up „nur“ noch reichlich halb so hoch.

Was zu bedenken ist

Um die versprochenen Performance-Gewinne zu erzielen, reicht es nicht, die GraalVM anstatt eines herkömmlichen JDKs zu installieren. Bei der Bytecode-Ausführung konnte – zumindest mit unseren Beispielen und unserem Setup – noch kein Performance-Gewinn erreicht werden. Dies ist erst mit einem Native Image möglich, welches gegenüber der Bytecode-Ausführung jedoch auch mehrere Nachteile hat, derer man sich bewusst sein sollte.

  • Im Native Image wird die SubstrateVM als JVM verwendet. Diese besitzt einige Einschränkungen. Abgesehen davon, dass derzeit noch nicht alle Features umgesetzt sind, stehen einige Dinge wie z. B. ein Security Manager gar nicht auf der Agenda.
  • Weiterhin sollte die Dauer des Build-Prozesses beachtet werden: Beim Native Image verschwindet die Startzeit nicht einfach. Die Rechenzeit wird mit verschiedenen Ansätzen „lediglich“ verlagert: Von der Ausführungszeit zur Build-Zeit. Die Erstellung unseres HelloWorld-Beispiels hat in unserer Umgebung reichlich drei Minuten benötigt – die Erstellung des HelloScript-Programmes dauerte bereits mehr als 20 Minuten (HelloScriptEngine: 1291s, HelloScriptPolyglot: 1251s).
  • Die größte Herausforderung ist aber die „Closed world assumption“. Bei der Erstellung des Native Image wird eine statische Codeanalyse durchgeführt – und nur die durchlaufenen Code-Teile werden in das Native Image kompiliert. Das funktioniert zwar für unser HelloWorld-Programm, aber bereits bei den JavaScript-Beispielen mussten Kommandozeilen-Parameter angegeben werden. Mittels „Reflection“ geladene Klassen werden nur erkannt, wenn der vollqualifizierte Klassenname festverdrahtet im Quellcode steht. Demzufolge gibt es Probleme mit allen Technologien, welche Dynamic Class Loading in irgendeiner Form nutzen: JNDI, JMX, …

Die dynamisch geladenen Programmteile können (und müssen) bei der Erstellung des Native Image explizit angegeben werden. Das schließt alle Programmteile ein: Neben dem eigenen Projekt-Code sind das auch alle eingesetzten Bibliotheken – bis hin zu denen der JRE. Da diese Konfiguration für „echte“ Programme eine wirkliche Herausforderung ist, werden hierzu Hilfswerkzeuge bereitgestellt, ohne die es in der Praxis vermutlich nicht funktioniert. Der Tracing Agent beispielsweise beobachtet ein als Bytecode ausgeführtes Programm. Er erkennt alle reflektiven Zugriffe und erzeugt daraus eine JSON-Konfiguration. Diese kann nun für die Erstellung des Native Image verwendet werden.

In der Praxis würde die Build-Pipeline also zunächst die Bytecode-Variante erstellen. Nun können alle automatisierten Tests mit dieser Bytecode-Variante ausgeführt werden, wobei der Tracing Agent die reflektiven Zugriffe erkennt. Unter der Annahme, dass dabei wirklich jeder Programmpfad ausgeführt wurde, kann nun das Native Image in einem weiteren Build-Schritt erzeugt werden. Dies führt direkt zum nächsten Punkt: Bei der Arbeit mit der Native-Image-Technologie wird der Build-Prozess insgesamt länger und komplexer.

Zusammenfassend bedeutet dies, dass beim Einsatz der Native-Image-Technologie einige Dinge kaum oder nicht möglich sind (bspw. Security Manager). Viele andere Dinge funktionieren zwar grundsätzlich, müssen aber umständlich konfiguriert werden. Hierfür ist eine Tool-Unterstützung gegeben und wird auch recht dynamisch weiterentwickelt. Die Hoffnung ist hier, dass die Mehraufwände (abgesehen von der Build-Dauer) durch die Werkzeuge kompensiert werden können. Allerdings wird dadurch der Build-Prozess auch komplexer und somit fehleranfälliger.

Fallstricke unter Windows

Abschließend noch ein Blick zur Windows-Plattform: Diese wird mittlerweile auch unterstützt. Vorbereitend für diesen Blogbeitrag wurden die Versionen „GraalVM Enterprise 20.3.0″ sowie „GraalVM Enterprise 21.0.0.2“ auf einem Windows-System betrachtet. Leider war hier die Dokumentation noch etwas lückenhaft und das Tooling greift noch nicht so gut ineinander wie in der Linux-Umgebung. Dadurch gab es auch Hindernisse, die unter Linux nicht aufgefallen sind. So trat beispielsweise ein Problem bei der Erstellung eines Native Images auf, wenn der zugrundeliegende Bytecode von einem anderen JDK (in dem Fall durch das OpenJDK) erzeugt wurde. Dabei ist die auftretende Fehlermeldung leider auch nicht so aussagekräftig, dass sie auf die eigentliche Ursache hinweist:

native-image de.zeiss.zdi.graal.HelloWorld

[de.zeiss.zdi.graal.helloworld:20764]    classlist:     947.02 ms,  0.96 GB
[de.zeiss.zdi.graal.helloworld:20764]        (cap):   3,629.54 ms,  0.96 GB
[de.zeiss.zdi.graal.helloworld:20764]        setup:   5,005.98 ms,  0.96 GB

Error: Error compiling query code (in C:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.c). Compiler command ''C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Tools\MSVC\14.28.29333\bin\HostX64\x64\cl.exe' /WX /W4 /wd4244 /wd4245 /wd4800 /wd4804 /wd4214 '-IC:\Program Files\Java\graalvm-ee-java11-21.0.0.2\include\win32' '/FeC:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.exe' 'C:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.c' ' output included error: [JNIHeaderDirectives.c, Microsoft (R) Incremental Linker Version 14.28.29337.0, Copyright (C) Microsoft Corporation.  All rights reserved., , /out:C:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.exe , JNIHeaderDirectives.obj , LINK : fatal error LNK1104: Datei "C:\Users\xyz\AppData\Local\Temp\SVM-13344835136940746442\JNIHeaderDirectives.exe" kann nicht ge?ffnet werden.]

Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
Error: Image build request failed with exit status 1

Ein weiterer Fallstrick bestand in der laufwerksübergreifenden Arbeit: Es ist unter Windows leider nicht möglich, die GraalVM auf einem Laufwerk zu installieren (in dem Fall unter C:\Programme) und auf einem anderen Laufwerk auszuführen (in dem Fall unter D:\dev\prj\…):

native-image de.zeiss.zdi.graal.HelloWorld

[de.zeiss.zdi.graal.helloworld:10660]    classlist:   3,074.80 ms,  0.96 GB
[de.zeiss.zdi.graal.helloworld:10660]        setup:     314.93 ms,  0.96 GB

Fatal error:java.lang.IllegalArgumentException: java.lang.IllegalArgumentException: 'other' has different root
        at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
[…]

Darüber hinaus konnten mit dem Native Image in der Windows-Umgebung leider keine Performance-Vorteile festgestellt werden. Derzeit ist die Windows-Unterstützung (sowohl die GraalVM-Toolchain selbst als auch die generierten Native Images) somit noch eher experimentell.

Fazit

Im Blogbeitrag wurde vor allem darauf eingegangen, dass die Startzeit der Java-Programme mit GraalVMs Native-Image-Technologie enorm verbessert werden kann. Es wurde dargestellt, welchen Ansätzen und welcher Techniken sich die GraalVM dabei bedient. Das Resultat wurde mit Messungen an Beispiel-Programmen untermauert. Auf der anderen Seite wurden aber auch einige Herausforderungen genannt, welche beim Einsatz der Native-Image-Technologie auftreten.

Mit länger laufenden Programmen sind vermutlich kaum Performance-Steigerungen zu erwarten, weil dann auch die Optimierungen der herkömmlichen JVMs greifen. Das ist erst einmal nur eine Behauptung. Eine Untersuchung dieses Aspekts würde den aktuellen Rahmen sprengen und hat genug Potenzial für einen eigenen Blogbeitrag.

Nun wollen wir uns noch kurz den Fragen aus der Einleitung zuwenden: Grundsätzlich ist das „herkömmliche“ Java schon lange nicht mehr langsam, sondern rasend schnell. Schon allein der Einsatz im (rechenintensiven) Big-Data-Umfeld ist ein Indiz hierfür. Die hauptsächliche Vorbedingung für diese hohe Performance ist eine gewisse Warmlaufzeit. Im Umkehrschluss heißt das: Der Start eines herkömmlichen Java-Programms lässt zu wünschen übrig – und genau dies ist der Hauptansatzpunkt der Native-Image-Technologie. Auf der anderen Seite bringt diese Technologie auch eine Menge Nachteile mit – vor allem für große, technik-reiche Anwendungen.

Zusammenfassend lässt sich sagen, dass die GraalVM das Potenzial hat, sich in verschiedenen Einsatzgebieten zu etablieren. Die polyglotten Eigenschaften können sich mehrsprachige Anwendungen zunutze machen und der Einsatz der betrachteten Native-Image-Technologie ist vor allem für Cloud Services durchaus denkbar. Allerdings wird sich der Einsatz der GraalVM vor allem für rechenintensive (d. h. meist auch länger laufende) nicht-triviale Anwendungen vermutlich nicht lohnen.

Abschließend soll noch als Vorteil für die GraalVM festgehalten werden, dass Compiler und Optimizer in Java implementiert sind. Das ist zwar zunächst grundsätzlich nicht besser oder schlechter als die bisherigen Implementierungen – wird aber mittel- bis langfristig die Chancen erhöhen, das Potential der Java-Community besser zu nutzen.

Alles in allem bleibt es wohl spannend: Eine grundsätzliche Ablösung des OpenJDK ist derzeit nicht absehbar. Und man darf schließlich auch nicht vergessen, dass die Entwicklung dort genauso wenig stillsteht. Allerdings hat die GraalVM durchaus das Potenzial, um sich (zunächst erst einmal?) in bestimmten Anwendungsgebieten zu etablieren.

Security und Compliance in Softwareprojekten – Dependencies unter Kontrolle bringen

Symbolbild: Weiße Tastatur mit Schloss und Schlüssel

Dieser Blogbeitrag befasst sich mit den hohen Ansprüchen an Security und Compliance, die wir an jedes Softwareprojekt stellen. Dafür verantwortlich ist in jedem Projekt ein ausgebildeter Security Engineer. Dabei stellen ihn insbesondere die unzähligen Dependencies in Softwareprojekten, welche in ihrer Vielzahl von Versionen unter Kontrolle gebracht werden müssen, vor große Herausforderungen.

Abbildung 1: Ein Ausschnitt aus dem Abhängigkeits-Graph eines npm Paketes, aus npmgraph.js.org/?q=mocha

Herausforderungen in Softwareprojekten

Große Softwareprojekte bestehen schon seit langer Zeit aus kleineren Teilen, die für ihr jeweiliges Gebiet wiederverwendet werden können. Komponenten, bei denen es nicht um geheime Funktionalität geht, werden zunehmend als „FOSS (Free and Open Source Software)“ veröffentlicht. Das bedeutet „quelloffen“ (Open Source) und mit einer freien Lizenz zur Weiterverwendung.

Dabei ist es für die Einschätzung und Prävention von Sicherheitslücken äußerst wichtig, eine vollständige Übersicht über alle eingebundenen Drittbibliotheken zu haben. Denn jedes unserer importierten Module kann ebenfalls mit mehreren Abhängigkeiten verbunden sein. Schnell steigt dann die Anzahl an zu beobachtenden Abhängigkeiten in die Tausende und es ist nicht einfach, zwischen allen Versionen den Überblick über Lizenzen und Sicherheitslücken zu behalten.

Die Auswirkung der Problematik wird z. B. klar, wenn man Fälle von „Supply chain attacks“ und „Dependency Hijacking“ der letzten Jahre liest. Eine interessante Meta-Analyse ist „What Constitutes a Software Supply Chain Attack? “ von Ax Sharma (https://blog.sonatype.com/what-constitutes-a-software-supply-chain-attack). Den Umgang mit diesen Komponenten in großen wie kleinen Softwareprojekten aus Sicht eines Security Engineers möchten wir weiter erläutern.

Lösungsmöglichkeiten mittels FOSS Scanner

Über die Zeit haben sich einige Projekte dem Problem der Kenntlichmachung von FOSS-Komponenten gewidmet. Es gibt Programme zum Erstellen von Bill of Material (BOM) und Übersichten zu Sicherheitsrisiken, welche wir verprobt haben.

Weiter gibt es große Kataloge wie den „Node Paketmanager“ (npm), die selbst ausführliche Informationen zu den jeweils angebotenen Komponenten geben.

Auch wenn es diese freien und quelloffenen Komponenten gratis gibt, so sind sie nicht ohne Aufwand, besonders in langlebigen und wichtigen Softwareprojekten.

Wir haben für die Evaluierung den OWASP-Dependency Check (DC) und das OSS Review Toolkit als kombinierte Lösung für das Auffinden von Sicherheitsproblemen mit DC und Überprüfung der Einhaltung der Lizenzbestimmungen eingesetzt. Im Vergleich zu kommerziellen Lösungen wie BlackDuck bieten diese frei und kostenlos die Möglichkeit einer Übersicht über die FOSS-Komponenten in Projekten und die Bewertung von Risiken.

Das war aber unserer Erfahrung nach mit Mehraufwand sowohl in der Konfiguration als auch bei der kontinuierlichen Überprüfung, d. h. neuen Scans auf neue Sicherheitsprobleme, verbunden.

Verantwortung als Software Engineer

Unsere Richtlinien für sichere Entwicklung und den Einsatz von Open Source geben die notwendigen Prozesse und Ziele vor, an dem sich unsere Security Engineers in Vertretung der Projekte orientieren. Der vielleicht wichtigste Ausschnitt daraus wird im folgenden Abschnitt aufgeführt:

It is our responsibility that the following so called Essential FOSS Requirements are fulfilled:

  • All included FOSS components have been identified and the fitness for purpose has been confirmed.
  • All licenses of the included FOSS have been identified, reviewed and compatibility to the final product/service offering has been verified. Any FOSS without a (valid) license has been removed.
  • All license obligations have been fulfilled.
  • All FOSS are continuously – before and after release – monitored for security vulnerabilities. Any relevant vulnerability is mitigated during the whole lifecycle.
  • The FOSS Disclosure Statement is available to the user.
  • The Bill of Material is available internally.

For that it must be ensured that

  • the relevant FOSS roles are determined and nominated.
  • the executing development and procurement staff is properly trained and staffed.

Anhand dieser Richtlinien werden verpflichtende Trainings, Wissensträger und Qualitätskontrollen gebildet.

Vorstellung der Abläufe

  • Untersuchen vor Einbindung (Lizenzen, Operational Risk wie Update-Häufigkeit)
  • Überwachen von Updates (Operational Risks)

Irgendwann soll eine neue Funktion zu einem Softwareprojekt hinzugefügt werden. Oft kennen Entwickler bereits mögliche FOSS Software, die bei der Funktionalität hilft.

Ein wichtiger Aspekt ist, dass möglichst jeder Entwickler den Umgang mit Paketmanagern und mögliche Implikationen kennt, um Ergebnisse aus den Tools oder Analysen richtig einordnen zu können. Es ist z. B. sehr wichtig, sich zu veranschaulichen, aus wie vielen Teilen eine Top-Level-Abhängigkeit besteht – oder verschiedene Abhängigkeiten gleicher Funktionalität im Hinblick auf zukünftige sichere Entwicklung (Operationelle Risiken) zu bewerten. Immer öfter sehen wir das Ziel, die Zahl an Abhängigkeiten klein zu halten. Das sollte bei der Auswahl von Komponenten berücksichtigt werden, um möglichst nur das wirklich notwendige an Funktionalität von zusätzlichen Abhängigkeiten zu erhalten.

Bereits vor dem Einbinden sind durch den Security Engineer potenzielle Imports auf ihre kompatible Lizenz und bestehende Sicherheitslücken zu überprüfen. Ebenso wichtig ist aber auch der Blick auf das, was unter operationale Risiken fällt wie z. B.:

  • Aktualität
  • Lebendige Community oder aktive Instandhaltung
  • Update-Zyklus ausreichend agil, um auftretende Sicherheitslücken zu beseitigen
  • Wird Wert auf den sicheren Umgang mit Abhängigkeiten gelegt?
  • Ist die Anzahl an weiteren Abhängigkeiten sinnvoll und wird wenn möglich reduziert?

Im laufenden Entwicklungsprozess und später im Betrieb muss das Projektteam auch informiert werden, wenn neue Sicherheitslücken entdeckt oder geschlossen werden. Dafür können periodische Scans oder eine Datenbank mit Alerts für Sicherheitslücken eingesetzt werden. Für periodische Scans spricht die größere Unabhängigkeit von der einen Datenbank – dafür müssen Hardware und Alerts selbst bereitgestellt werden. Diese wiederum sind einer der Mehrwerte einer Software-Composition-Analysis-Lösung wie BlackDuck.

Da der Anteil an gut gekennzeichneter FOSS steigt, wird bei neuen Versionen der Zeitaufwand für manuelle Kuration vergleichsweise geringer. Dazu zählen das Deklarieren einer Lizenz – und leicht auffindbare und formatierte Copyright-Angaben in den Komponenten, was in älteren Komponenten oft sehr individuell formatiert oder ganz weggelassen wurde. Ist keine Lizenz angegeben, so darf dies nicht fälschlicherweise als „Freibrief“ verstanden werden. Ohne eine Lizenz darf eine Komponente nicht ohne Einverständnis der Autoren benutzt werden!

Beispiel einer Sicherheitslücke

Ein Beispiel für eine komplizierte Sicherheitslücke ist unter dem CVE-2021-32796 veröffentlicht worden. Eingebunden wird das problematische Modul xmldom indirekt über zwei weitere Abhängigkeiten in unserem Beispielprojekt.

BlackDuck zeigt uns zu dem Modul folgende Sicherheitswarnung:

Abbildung 2: BlackDuck: Beispiel Zusammenfassung einer Schwachstelle  

Damit kann der Security Engineer bereits eine grobe Einschätzung zur Tragweite der Sicherheitslücke vornehmen. Auch ist ein Hinweis auf dem Patch in Version 0.7.0 angegeben.

Wichtigkeit von Vorlauf für Updates/Austausch von Kompetenzen

Wir haben in der Zeit bis zu der „frischen Veröffentlichung“ unter @xmldom/xmldom bereits überprüfen können, welchen Aufwand es bedeuten würde, ohne diese Abhängigkeit auszukommen.

Um diese Zeit zu haben, ist es sehr nützlich, bereits im Entwicklungsprozess – und mit genügend Vorlauf zu einer Produktveröffentlichung – eine Übersicht über mögliche Probleme zu bekommen.

Das erleichtert den Entwicklern das Evaluieren von Ausweichlösungen für problematische Software-Bibliotheken, sei es wegen Sicherheitslücken, inkompatiblen Lizenzen oder anderer operativer Risiken.

Fazit

Dieser Beitrag hat einen Überblick über unsere Arbeit mit der großen Vielfalt an Open Source in unseren Projekten und die Aufgaben als Security Engineer im Umgang mit Open Source gegeben. Damit bringen wir mittels moderner Werkzeuge die Vielfalt an Abhängigkeiten unter Kontrolle und schaffen die notwendige Transparenz und Sicherheit. Bereits vor Einbinden von Abhängigkeiten sollte eine Evaluierung dieser von einem geschulten Team durchgeführt werden, und danach während des ganzen Software-Lebenszyklus überwacht und auf Probleme reagiert werden.

Testautomatisierungstool aus dem Katalog – Wie finde ich das perfekte Werkzeug?

Softwaresysteme werden durch die stetig wachsende Anzahl von Anwendungen auf unterschiedlichen Plattformen immer komplexer. Ein entscheidender Faktor für den Erfolg eines Softwareprodukts ist dessen Qualität. Daher führen immer mehr Unternehmen systematische Prüfungen und Tests möglichst auf den verschiedenen Plattformen durch, um einen vorgegebenen Qualitätsstandard sicherstellen zu können. Um trotz des höheren Testaufwands kurze Release-Zyklen einhalten zu können, wird es notwendig, die Tests zu automatisieren. Dies wiederum führt dazu, dass eine Testautomatisierungsstrategie definiert werden muss. Einer der ersten Schritte bei der Einführung einer Testautomatisierungsstrategie ist die Evaluierung von geeigneten Testautomatisierungswerkzeugen. Da jedes Projekt einzigartig ist, variieren sowohl die Anforderungen als auch die Wahl der Werkzeuge. Diese Blogreihe soll eine Hilfestellung bei der Auswahl der passenden Lösung geben.

Symbolbild: Zwei Personen sitzen am Tisch und schauen auf ein Tablet, unter dem Tablet liegen ausgedruckte Statistiken sowie Notizbücher auf dem Tisch.
Abbildung 1: Zur Einführung einer Testautomatisierungsstrategie gehört auch die Evaluierung geeigneter Testautomatisierungswerkzeuge

Einführung

Im Rahmen meiner Abschlussarbeit übernahm ich die Aufgabe, den Scrum-Teams der ZEISS Digital Innovation (ZDI) ein Hilfsmittel an die Hand zu geben, mit dem sie das passende Testautomatisierungswerkzeug schnell und flexibel finden können. Die Herausforderung bestand dabei darin, dass die Projekte spezifische Szenarien besitzen und die Anforderungen gegebenenfalls gesondert gewichtet werden müssen. Den Stand der Arbeit und die Ergebnisse möchte ich euch in diesem und den nächsten Blogartikeln vorstellen.

Die Softwareentwicklung ist schon seit langem ein Bereich, der sich schnell verändert. Doch während diese Entwicklungen in der Vergangenheit vor allem auf technologischer Ebene stattgefunden haben, beobachten wir derzeit größere Veränderungen im Bereich der Prozesse, der Organisation und der Werkzeuge in der Softwareentwicklung. Diese neuen Trends sind jedoch mit Herausforderungen verbunden wie z. B. Änderungen im Anforderungsmanagement, verkürzten Timelines und speziell den gestiegenen Anforderungen an die Qualität. Durch die Entwicklung bedarfsgerechter Lösungen und die Optimierung der Qualität werden heute sowohl die Effizienz als auch die Effektivität innerhalb der Softwareentwicklung gesteigert.

Zudem werden Softwaresysteme durch die stetig wachsende Anzahl von Anwendungen auf unterschiedlichen Plattformen immer komplexer. Da die Qualität ein entscheidender Faktor für den Erfolg eines Softwareprodukts ist, führen immer mehr Unternehmen systematische Prüfungen und Tests möglichst auf den verschiedenen Plattformen durch, um einen vorgegebenen Qualitätsstandard sicherzustellen. Eine Anfang 2021 durchgeführte SmartBear-Umfrage ergab, dass viele Unternehmen, unabhängig von der Branche, bereits verschiedene Arten von Tests durchführen, wobei das Testen von Webanwendungen mit 69 % an erster Stelle und das Testen von API/Webdiensten mit 61 % an zweiter Stelle steht. Das Desktop-Testing wird von 42 % der Befragten durchgeführt. Insgesamt 62 % der Befragten geben an, dass sie mobile Tests durchführen, davon 29 % für native Applikationen (Apps) (SmartBear, 2021, S. 12). Um trotz des höheren Testaufwands kurze Release-Zyklen einhalten zu können, wird es notwendig, die Tests zu automatisieren.

Als ZDI unterstützen wir unsere Kunden innerhalb und außerhalb der ZEISS Gruppe bei ihren Digitalisierungsprojekten. Zusätzlich bieten wir individuelle Testservices an. Darum gibt es bei uns eine Vielzahl von Projekten mit unterschiedlichen Technologien und unterschiedlichen Lösungen. Als kleine Auswahl sind hier Schlagwörter wie Java, .Net, JavaFX, WPF, Qt, Cloud, Angular, Embedded etc. zu nennen. Bei solchen komplexen Projekten steht die Qualitätssicherung immer im Vordergrund und die Projekte sind abhängig vom Einsatz moderner Testwerkzeuge, welche die Projektbeteiligten bei den manuellen, aber besonders bei den automatisierten Tests unterstützen. Dabei wäre ein Werkzeug wünschenswert, das diese Automatisierung effektiv und effizient unterstützt. Testerinnen und Tester stehen jedoch bei der Auswahl eines Testautomatisierungswerkzeugs vor einer Vielzahl von Herausforderungen.

Herausforderungen

Bei der Recherche und den Interviews im Rahmen meiner Arbeit wurden als größte Herausforderungen bei der Testautomatisierung die Vielfalt der verfügbaren Testwerkzeuge und die fortschreitende Fragmentierung des Marktes genannt. Aufgrund dieser Fragmentierung stehen für den gleichen Einsatzzweck eine Vielzahl von Werkzeugen bereit.

Die Auswahl wird noch schwieriger, da sich die Werkzeuge nicht nur in der Technologie unterscheiden, die sie testen können, sondern auch im Arbeitsansatz. Wenn von Automatisierung die Rede ist, wird sie immer mit Skripting in Verbindung gebracht. In den letzten Jahren wurde jedoch ein neuer Ansatz für GUI-Tests entwickelt, der als NoCode/LowCode bezeichnet wird. Dieser Ansatz erfordert grundsätzlich keine Programmierkenntnisse und ist daher auch bei Automatisierungseinsteigern beliebt. Trotzdem bleibt das Skripting die dominierende Methode, obwohl manchmal beide Ansätze verwendet werden, um möglichst viele aus dem Qualitätssicherungsteam einzubeziehen (SmartBear, 2021, S. 33).

Die Art und Weise des Testautomatisierungsansatzes und damit der Einsatz eines Testautomatisierungswerkzeugs sind abhängig von den Anforderungen im Projekt. Dies bedeutet, dass die Anforderungen bei jedem neuen Projekt immer wieder neu geprüft werden müssen.

Bestandsaufnahme

Im Rahmen der Interviews, der Analyse des aktuellen Vorgehens und der Auswertung einer internen Umfrage konnte ich folgende Vorgehensweisen für die Auswahl der Werkzeuge in den Projekten identifizieren, die sich als „Quasi-Standard“ etabliert haben:

  • Das Werkzeug ist Open Source und kostet nichts.
  • Es wurde mit dem Werkzeug schon einmal gearbeitet und es wurde als vertrauenswürdig eingestuft.

Ein Ziel der Umfrage war es, herauszufinden inwieweit die Projektsituation einen Einfluss auf die Toolauswahl hat. Darum wurden im nächsten Schritt die Projekt-Situationen bzw. die verwendeten Vorgehensmodelle der Projekte und deren Einfluss untersucht. Es zeigte sich, dass nicht die Vorgehensmodelle einen direkten Einfluss auf die Toolauswahl haben, sondern die Personen oder Gruppen, die als zukünftige Anwender das Werkzeug nutzen oder die den Einsatz freigeben.

Bei der Überprüfung des Einflusses dieser operativ-eingebundenen Beteiligten zeigte sich, dass es noch weitere Interessengruppen gibt, die direkt oder indirekt einen Einfluss auf die Auswahl eines Werkzeuges haben. Dies sind z. B.:

  • Softwarearchitektinnen und Softwarearchitekten, die die Technologie oder Tool-Ketten innerhalb des Projektes definieren,
  • das Management, das Vorgaben festlegt für den Einkauf oder die Verwendung der Werkzeuge (OpenSource, GreenIT, strategische Partnerschaften etc.) oder
  • die IT des Unternehmens, die durch die verwendete Infrastruktur den Einsatz bestimmter Werkzeuge verhindert (FireWall, Cloud etc.).

Darüber hinaus fanden sich bei den Recherchen bereits erste Ansätze von Checklisten für die Auswahl von Testwerkzeugen. Sie basierten meist auf wenigen funktionalen Kriterien und wurden in Workshops durch die Projektmitglieder bestimmt. Die Auswertung zeigte, dass es keine einheitliche Methode gibt und dass die so ausgewählten Tools sehr oft später durch andere Werkzeuge ersetzt wurden. Dies wurde notwendig, da bei der Auswahl notwendige Anforderungen an das Werkzeug übersehen wurden.

In der Mehrzahl der Fälle waren die vergessenen Anforderungen nicht-funktionaler Natur, wie z. B. Kriterien der Usability oder Anforderungen an die Performance. Darum war ein nächster Schritt bei der Prüfung relevanter Kriterien, die ISO/IEC 25000 Software Engineering (Qualitätskriterien und Bewertung von Softwareprodukten) heranzuziehen.

Nicht immer, aber sehr oft, waren die übersehenen Anforderungen nicht-funktionaler Natur. Darum war ein nächster Schritt bei der Prüfung relevanter Kriterien, die ISO/IEC 25000 Software Engineering (Qualitätskriterien und Bewertung von Softwareprodukten) heranzuziehen.

Schematische Darstellung der Kriterien der ISO/IEC 25000 Software Engineering
Abbildung 2: Kriterien für Software nach ISO/IEC 25010

Im nächsten Blogartikel dieser Reihe wird aufgezeigt, wie der Kriterienkatalog aufgebaut ist und wie sich die finale Liste der Kriterien zusammensetzt.


Literatur

SmartBear (2021): State of Software Quality | Testing.

Dieser Beitrag wurde fachlich unterstützt von:

Kay Grebenstein

Kay Grebenstein arbeitet als Tester und agiler QA-Coach für die ZEISS Digital Innovation, Dresden. Er hat in den letzten Jahren in Projekten unterschiedlicher fachlicher Domänen (Telekommunikation, Industrie, Versandhandel, Energie, …) Qualität gesichert und Software getestet. Seine Erfahrungen teilt er auf Konferenzen, Meetups und in Veröffentlichungen unterschiedlicher Art.

Alle Beiträge des Autors anzeigen

Application Security Tools: Nutzung für Static Application Security Testing & Software Composition Analysis

Im Bereich der Application Security gibt es verschiedene Konzepte, um das Ziel zu erreichen, zuverlässige und sichere Software zu entwickeln. In diesem Beitrag werden das Static Application Security Testing (SAST) und die Software Composition Analysis (SCA) als wichtige Bestandteile der Application Security vorgestellt. Zur Umsetzung dieser Konzepte und Erhöhung der Application Security können entsprechende Tools eingesetzt werden.

Symbolbild: Laptop mit Log-In Maske

SAST Tools

Static Application Security Testing dient dazu, den Code einer Software auf Schwachstellen und Sicherheitslücken zu untersuchen.

Funktionsweise der SAST Tools

Als ein Teil der statischen Codeanalyse fokussiert SAST die Einhaltung von Secure Coding Richtlinien und generellen Programmierstandards. Beispielsweise können Entwicklerinnen und Entwickler dabei unterstützt werden, Schwachstellen, die in der Top 10 des Open Web Application Security Project (OWASP) oder der Top 25 der Common Weakness Enumeration (CWE) aufgelistet sind, zu vermeiden.

SAST Tools sind Teil des White-Box Testings, denn sie benötigen Zugriff auf den Quellcode einer Applikation. Idealerweise wird SAST an zwei Stellen in der Softwareentwicklung eingesetzt. Zunächst in der Entwicklungsumgebung (IDE) der Softwareentwicklerinnen und Softwareentwickler, um möglichst zeitnah potenzielle Sicherheitslücken während des Schreibens von Programmcode aufzuspüren. Außerdem ist es sinnvoll, das eingesetzte SAST-Tool in den Prozess zur Continuous Integration/Continuous Delivery (CI/CD) zu integrieren. Auf diese Weise kann eine statische Sicherheitsanalyse des gesamten Quellcodes erfolgen. Zusätzlich kann sichergestellt werden, dass nur ein durch das SAST-Tool geprüfter Code die weiteren Stufen des Entwicklungsprozesses wie Test und Deployment erreicht. Je früher im Softwareentwicklungsprozess eine Schwachstelle oder Sicherheitslücke gefunden wird, desto kostengünstiger ist es, sie zu beheben.

Einige SAST Tools stellen weitere Funktionalitäten bereit, wie die Ausführung von Unit- und Integrationstests. Der Vollständigkeit halber muss erwähnt werden, dass SAST nur ein einzelner Baustein zu einer sicheren Applikation ist und durch weitere Überprüfungs- und Testmethoden wie Code Reviews, Dynamic Application Security Testing (DAST), Interactive Application Security Testing (IAST) und manuelle Tests unbedingt ergänzt werden sollte.

SCA Tools

Software Composition Analysis dient dazu, den Einsatz von freier und Open Source Software in Softwareentwicklungsprojekten zu vereinfachen und sicher zu gestalten. Free and Open Source Software (FOSS) ist seit Langem ein wichtiger Bestandteil in vielen Softwareprojekten, um wiederkehrende Aufgaben durch zuverlässige und einsatzerprobte Softwarekomponenten abzubilden.

Herausforderungen beim Einsatz von FOSS

Allerdings ergeben sich durch den Einsatz von FOSS verschiedene Risiken, die den applikations- und rechtssicheren Betrieb der entwickelten Software betreffen. Auch FOSS-Komponenten haben Sicherheitsschwachstellen, die erst im Laufe der Entwicklung oder des Betriebs einer Software entdeckt werden. Des Weiteren ist FOSS durch rechtlich bindende Lizenzen geschützt, die teils erhebliche Einschränkungen für die Verwendung mit sich bringen.

Unter anderem aus diesen Gründen ist es wichtig zu wissen, welche FOSS man in welcher Version einsetzt und unter welcher Lizenz diese veröffentlicht wurde. Hierbei unterstützen SCA Tools, indem sie diesen Prozess automatisieren.

Gründe für den Einsatz von SCA Tools

Umfangreiche und empfehlenswerte SCA Tools sind in der Lage, mehrere Millionen FOSS-Komponenten zu identifizieren. Dadurch ist es möglich, detaillierte Bill of Materials (BoM) zu erstellen, die einen exakten Einblick in die verwendete FOSS gewährleisten. Anhand dieser Informationen können nachfolgend Schwachstellen und Sicherheitslücken aus vielen verschiedenen Quellen ermittelt werden. Im Anschluss erstellen gute SCA Tools Risiko- und Dringlichkeitsbewertungen, die durch Empfehlungen zur Problemlösung (FOSS-Update oder Workaround) ergänzt werden. Somit kann innerhalb kürzester Zeit auf neue Bedrohungen für die eigene Applikation reagiert werden.

Viele SCA Tools lassen sich in stark automatisierte Prozesse wie Continuous Integration/Continuous Delivery (CI/CD) oder DevSecOps einbinden. Damit ist es möglich, bereits während der Applikationsentwicklung auf FOSS-Schwachstellen zu reagieren. Durch automatisierte Scans, die Teil des Build-Prozesses sind, besteht auch hier die Nachbesserungsmöglichkeit, bevor die Applikation getestet oder deployed wird. Aus Security-Sicht ist es dringend zu empfehlen, dass die im Betrieb befindliche Software in regelmäßigen Abständen durch das eingesetzte SCA-Tool gescannt wird. So kann auch in dieser Phase des Software-Lebenszyklus die Sicherheit der Applikation in Bezug auf die integrierten FOSS-Komponenten gewährleistet werden.

Lizenzen, unter denen FOSS bereitgestellt wird, sind oftmals komplex und können den Einsatz oder Vertrieb der Applikation einschränken. Ein professionelles SCA-Tool erkennt mehrere tausend Lizenzen und unterstützt bei der Identifizierung von Lizenzbedingungen und deren Auswirkungen. Zusätzlich sollten Konflikte zwischen unterschiedlichen FOSS-Lizenzen ermittelt und als Warnung ausgegeben werden können. Weiterhin kann die Unterstützung bei der Generierung des FOSS Disclosure Statements zum Funktionsumfang gehören.

Fazit

Sichere Software zu entwickeln, ist ein zeit- und arbeitsintensiver Prozess. Durch die Integration von SAST- und SCA Tools in den Entwicklungsprozess können viele manuelle Tätigkeiten automatisiert und gleichbleibend gründlich durchgeführt werden. Aus diesen und den vielen anderen oben genannten Gründen ist der Einsatz solcher Werkzeuge ein elementarer Bestandteil der Application Security. Deshalb ist er – auch in unseren Softwareentwicklungsprojekten bei der ZEISS Digital Innovation – unerlässlich, um zuverlässige, qualitativ hochwertige und vor allem sichere Software zu entwickeln.

Der Einsatz eines SAST Tools und eines eines SCA Tools ist ein elementarer Bestandteil der Application Security. Somit ist er – auch in unseren Softwareentwicklungsprojekten bei der ZEISS Digital Innovation – unerlässlich, um zuverlässige, qualitativ hochwertige und v. a. sichere Software zu entwickeln.

Patientenversorgung der Zukunft – Digital Health Solutions mit Azure Health Data Services

Seit Beginn der Covid-Pandemie steht das Gesundheitswesen unter enormem Druck. Die demografische Entwicklung, der Wandel des Krankheitsspektrums, gesetzliche Vorschriften, Kostendruck und Fachkräftemangel bei gleichzeitig steigendem Anspruch der Patienten stellt Gesundheitsorganisationen vor einige Herausforderungen. Hier bieten die Digitalisierung und der Einsatz moderner Technologien wie Künstlicher Intelligenz oder Machine Learning zahlreiche Chancen und Potentiale zur Effizienzsteigerung, Fehlerreduktion und somit Verbesserung der Patientenbehandlung.

Arzt verwendet eine medizinische Applikation auf Cloud-Basis auf dem Smartphone, im Hintergrund unterhalten sich medizinische Fachkräfte
Abbildung 1: Digital Health Solutions mit Azure Health Data Services für eine optimale und zukunftsfähige Patientenversorgung

Nutzung medizinischer Daten als Basis für eine optimierte Patientenversorgung

Grundlage für die Nutzung dieser Technologien und für eine zukunftsfähige prädiktive und präventive Versorgung sind medizinische Daten. Diese finden sich bereits heute überall. Die meisten Akteure im Gesundheitswesen und die im Einsatz befindlichen Medizingeräte speichern diese aber immer noch on-premise, was zu Millionen von isolierten medizinischen Datensätzen führt. Um einen vollumfänglichen Überblick über die Krankheitsgeschichte eines Patienten zu erhalten und darauf aufbauend Behandlungspläne im Sinne einer patientenzentrierten Behandlung zu erstellen und übergreifende Erkenntnisse aus diesen Datensätzen ableiten zu können, müssen Organisationen Gesundheitsdaten aus verschiedenen Quellen integrieren und synchronisieren.

Um die Entwicklung von Ökosystemen im Gesundheitswesen zu unterstützen, bieten die großen globalen Public Cloud Provider (Microsoft Azure, Amazon Web Service und Google Cloud Platform) zunehmend spezielle SaaS- und PaaS-Dienste für den Healthcare-Sektor an, die Unternehmen eine Basis für ihre eigenen Lösungen bieten können. Durch unsere Erfahrung bei ZEISS Digital Innovation als Implementierungspartner der Carl Zeiss Meditec AG und von Kunden außerhalb der ZEISS Gruppe haben wir bereits früh erkannt, dass Microsoft ein besonders leistungsfähiges Healthcare-Portfolio bietet und dieses weiter stark ausbaut. Das wurde auch auf der diesjährigen Ignite wieder deutlich.

Screenshot aus einem Video, in dem sich zwei Personen virtuell zu einem Thema unterhalten
ZEISS Digital Innovation (re.) bei der Ignite 2021 im Gespräch darüber, wie man langfristigen Nutzen aus Gesundheitsdaten mit Microsoft Cloud for Healthcare ziehen kann. (Hier geht’s zum vollständigen Video)

Medizinische Datenplattformen auf Basis von Azure Health Data Services

Eine Möglichkeit für den Aufbau einer solchen medizinischen Datenplattform als Basis eines Ökosystems ist die Nutzung vonAzure Health Data Services. Mit Hilfe dieser Services lassen sich Speicherung, Zugriff und Verarbeitung von medizinischen Daten interoperabel und sicher gestalten. Tausende Medizingeräte können miteinander verbunden werden und die so generierten Daten von zahlreichen Anwendungen skalierbar und robust genutzt werden. Da es sich bei den Azure Health Data Services um PaaS-Lösungen handelt, sind sie „out of the box“ nutzbar und werden vollständig von Microsoft entwickelt, verwaltet und betrieben. Sie sind dadurch außerdem standardmäßig sicher und compliant und mit geringem Aufwand hoch verfügbar. Dies senkt den Implementierungsaufwand und damit auch die Kosten erheblich.

Auch die Carl Zeiss Meditec AG setzt bei der Entwicklung seines digitalen, datengesteuerten Ökosystems auf Azure Health Data Services. Das gemeinsam mit der ZEISS Digital Innovation entwickelte ZEISS Medical Ecosystem verbindet Geräte und klinische Systeme über eine zentrale Datenplattform mit Applikationen und schafft so einen Mehrwert auf verschiedenen Ebenen zur Optimierung des klinischen Patientenmanagements.

Als zentrale Schnittstelle für die Geräteanbindung wird hier der DICOM Service der Azure Health Data Services genutzt. Da DICOM ein offener Standard zur Speicherung und zum Austausch von Informationen im medizinischen Bilddatenmanagement ist, kommunizieren ein Großteil der medizinischen Geräte, die Bilddaten generieren, mithilfe des DICOM-Protokolls. Durch eine erweiterbare Konnektivitätslösung, die auf Azure IoT Edge basiert, können diese Geräte unter Verwendung des DICOM-Standards direkt mit der Datenplattform in Azure verbunden werden. Dies ermöglicht die Integration einer Vielzahl von Geräten, die bereits seit Jahren bei Kunden im Einsatz sind, in das Ökosystem. Dies erhöht die Akzeptanz und sorgt dafür, dass mehr Daten in die Cloud fließen und weiterverarbeitet werden können, um klinische Anwendungsfälle zu ermöglichen und neue Verfahren zu entwickeln.

Als zentraler Data Hub der Plattform dient Azure API for FHIR®. Dort werden alle Daten des Ökosystems strukturiert gespeichert und miteinander verknüpft, um sie zentral auffindbar zu machen und den Applikationen zur Verfügung zu stellen. HL7® FHIR® (Fast Healthcare Interoperability Resources) bietet ein standardisiertes und umfassendes Datenmodell für Daten im Gesundheitsweisen. Damit können nicht nur einfache und robuste Schnittstellen zu den eigenen Anwendungen umgesetzt werden, sondern auch die Interoperabilität mit Drittsystemen wie EMR-Systemen (Electronic Medical Record), Krankenhausinformationssystemen oder der elektronischen Patientenakte sichergestellt werden. Die Daten aus den medizinischen Geräten, historische Messdaten aus lokalen PACS-Lösungen und Informationen aus anderen klinischen Systemen werden nach dem Hochladen automatisch weiterverarbeitet, strukturiert und zentral in Azure API for FHIR® aggregiert. Dies ist ein Schlüsselfaktor, um mehr wertvolle Daten für klinische Anwendungsfälle zu sammeln und den Kunden ein nahtlos integriertes Ökosystem zu bieten.

Schematische Darstellung des Aufbaus einer medizinischen Datenplattform mit Azure Healthcare APIs
Abbildung 2: Aufbau einer medizinischen Datenplattform mit Azure Health Data Services

Erfolgreiche Zusammenarbeit von ZEISS Digital Innovation und Microsoft

Als Early Adopter der Azure Health Data Services arbeiten unsere Entwicklungsteams bei ZEISS Digital Innovation eng mit der Produktgruppe der Azure Health Data Services im Microsoft Headquarter in Redmond, USA, zusammen und gestalten so die Services im Sinne unserer Kunden mit. In regelmäßigen Co-Creation-Sessions zwischen den Teams der ZEISS Digital Innovation und Microsoft wird das Lösungsdesign für aktuell implementierte Features der Azure Health Data Services diskutiert. So können wir sicherstellen, dass auch die komplexesten derzeit bekannten Anwendungsfälle berücksichtigt werden.

We are working very closely with ZEISS Digital Innovation to shape Azure’s next generation health services alongside their customer needs. Their strong background in the development of digital medical products for their customers is a core asset in our collaboration and enables the development of innovative solutions for the healthcare sector.

Steven Borg (Director, Medical Imaging at Microsoft)

Profitieren auch Sie von unserem Know-how und kontaktieren Sie uns. Wir erarbeiten gemeinsam die Vision Ihrer innovativen Lösung und unterstützen Sie bei der Implementierung.

Dieser Beitrag wurde verfasst von:

Elisa Kunze

Elisa Kunze ist seit 2013 bei der ZEISS Digital Innovation tätig und konnte dort bei ihren unterschiedlichen Tätigkeiten im Sales und Marketing viele Projekte, Teams und Unternehmen in unterschiedlichsten Branchen kennenlernen und begleiten. Heute betreut sie als Key Account Managerin ihre Kunden im Health Sector bei der Umsetzung ihrer Projektvisionen.

Alle Beiträge des Autors anzeigen

Mit Matrizen bewerten – Die perfekte Entscheidungsmatrix

Ein nicht unerheblicher Teil der Arbeit eines Softwarearchitekten besteht darin, verschiedene Lösungsalternativen miteinander zu vergleichen. Hierbei wird oft auf Entscheidungstabellen bzw. Bewertungsmatrizen zurückgegriffen, wobei beide Begriffe in aller Regel synonym verwendet werden. Dieser Artikel soll einen Einblick in zwei grundlegende Vorgehen bieten und jene nach ihrer Eignung bewerten.

Symbolbild: Blick auf die Füße einer Person in weißen Turnschuhen, die auf einer asphaltierten Straße auf einem aufgemalten weißen Pfeil steht, der in zwei verschiedene Richtungen zeigt
Abbildung 1: Softwarearchitekten müssen oft verschiedene Lösungsalternativen miteinander vergleichen. Dafür nutzen sie oft bestimmte Bewertungsmatrizen.

Arten von Bewertungsmatrizen

Bewertungsverfahren, um mehrere zur Auswahl stehende Varianten miteinander zu vergleichen, reichen von einfachen, über Abstimmung entstandene Rankings bis hin zu komplizierten Bewertungsverfahren über Matrizenrechnungen. Die Herausforderung besteht darin, für den Vergleich zwei zur Auswahl stehender Optionen die am besten geeignete Methodik zur objektivierten Bewertung zu finden. Kriterien hierfür sind:

  • Schneller Vergleich
  • Einfaches, unkompliziertes Verfahren
  • Geringe bis keine Einarbeitungszeit
  • Beliebige Personenanzahl

Nach kurzem Vergleichen der Bewertungsverfahren anhand der genannten Kriterien stellt sich hier die sehr verbreitete und bekannte Nutzwertanalyse als das Mittel der Wahl heraus. Warum? Weil sich mit ihr unkompliziert und einfach Optionen anhand verschiedener Kriterien vergleichen lassen, ohne dass es eines großen mathematischen Aufwandes bedarf. Die Varianten werden bei der Nutzwertanalyse mithilfe gewichteter Kriterien mit einem Score bewertet, Gewichtung und Score miteinander multipliziert und alle Bewertungen für eine Option addiert (siehe Beispiel).

Tabelle entsprechenden Kriterien
Abbildung 2: Bewertung der einzelnen Varianten in einer Nutzwertanalyse

Dieses Verfahren sollte fast jedem geläufig sein und findet auch in der Praxis in vielen Bereichen Anwendung. Neben der Bewertung durch die bewertende Person ergibt sich eine potenzielle subjektive Fehlerquelle: die Gewichtung der Kriterien. Da die Score-Vergebung nicht objektiver gestaltet werden kann, muss eine Möglichkeit der objektiven Gewichtungsberechnung gefunden werden. Diese ist absolut unumgänglich, wenn eine aussagekräftige Nutzwertanalyse gefordert ist. Die objektivierte Gewichtung stellt sicher, dass keine durch z. B. Zeitdruck entstandenen, „kopflosen“ Entscheidungen getroffen werden und die Gewichtung möglichst unabhängig vom Betrachtenden ist.

Verfahren zur Gewichtungsberechnung

Wie schon bei der Nutzwertanalyse besteht hierbei der Anspruch, dass das Verfahren unkompliziert, schnell und ohne Einarbeitungszeit erfolgt. Unter diesen Prämissen kristallisieren sich insbesondere zwei Verfahren heraus, die im Folgenden kurz erläutert werden.

Abgestufte Gewichtung

Bei der abgestuften Gewichtungsberechnung werden alle Kriterien hinsichtlich ihrer Bedeutung untereinander verglichen. Dabei umfasst die Skala fünf Abstufungen von -2: „wesentlich kleinere Bedeutung“ bis hin zu 2: „wesentlich größere Bedeutung“. Für jede Paarung von Kriterien muss daher diese granulare Einschätzung erfolgen. Über eine an Matrizenrechnung angenähertes Verfahren wird dann die Gewichtung berechnet.

Skala mit Legende
Abbildung 3: Beispiel für eine Skala bei einer abgestuften Gewichtung

Prioritätengewichtung

Hierbei wird auf die granulare Bewertung verzichtet und lediglich unterschieden in „wichtiger“ oder „unwichtiger“. Somit wird jedes Kriterium mit jedem anderen verglichen und das wichtigere in der Tabelle notiert. Über den relativen Anteil der Anzahl eines Kriteriums wird dann die Gewichtung ermittelt. Diese Vorgehensweise lässt sich gut im Team integrieren, da man nach dem Ranking durch die Einzelpersonen alle Ergebnisse zusammenführen kann und somit eine repräsentative Gewichtung erhält.

Tabelle mit verschiedenen Kriterien
Abbildung 4: Beispiel für eine Prioritätengewichtung einzelner Kriterien

Wann ist welches Verfahren geeignet?

Prinzipiell kann jedes Verfahren zur Gewichtungsberechnung in jeder Situation angewendet werden. Die folgende Tabelle gibt aber Anhaltspunkte, bei welchen Gegebenheiten welche Berechnungsart vorteilhaft sein kann.

Abgestufte GewichtungPrioritätengewichtung
Geringe Anzahl an KriterienHohe Anzahl an Kriterien
Geringe Anzahl an bewertenden PersonenHohe Anzahl an bewertenden Personen
Ausreichend Zeit verfügbarWenig Zeit verfügbar
Besonders wichtige Entscheidung 

Zusammenfassend kann gesagt werden, dass das Verfahren der abgestuften Gewichtung häufig zu aufwändig ist, wenn man in der Praxis davon ausgeht, dass Entscheidungen teilweise nicht einmal von einem repräsentativen Bewertungsverfahren begleitet werden. Die Prioritätengewichtung hingegen ist eine unkomplizierte, schnell verständliche und auch im Team implementierbare Möglichkeit und wird daher besonders empfohlen.

Neben der Nutzwertanalyse können auch noch weitere Verfahren und Kennzahlen zum Variantenvergleich hinzugezogen werden, beispielweise Standardabweichung, Anzahl Gewinne/Verluste usw., welche die schlussendliche Entscheidung leichter machen sollen, aber nicht Bestandteil dieses Beitrags sind.

Testdaten mit Oracle SQL erzeugen

Viele Projekte arbeiten mit Datenbanken und da muss natürlich die Performance des Systems getestet werden. Meist werden kleinere Datenmengen durch manuelle Tests eingegeben, aber wie erhält man Massendaten? 

Die übliche Antwort lautet natürlich „Kauf eines der Tools“ – und meist zu Recht. Diese Tools haben eine (oft) recht einfach bedienbare UI, sind flott und vor allem können sie semantisch sinnvolle Daten generieren.  

Zwei Personen an einem Bildschirm, arbeiten gemeinsam an einer Entwickler-Oberfläche
Abbildung: Um einen aussagekräftigen Performance-Test für Datenbanken durchzuführen, müssen Testerinnen und Tester die Verarbeitung sowohl kleiner als auch großer Datenmengen simulieren.

Daten generieren mit SQL

Dennoch, mit ein wenig SQL kann man auch selbst derartige Daten generieren. Interessant wird es bei der Verknüpfung von mehreren Tabellen, was im folgenden Beispiel eine wichtige Rolle spielt. 

Szenario: Unser Projekt verwaltet Firmen und deren Anlagen und für diese sollen Rechnungsdaten generiert werden. Für diese Daten gibt es im Projekt verschiedene Ansichten und Abfragen. Wir möchten einige Bereiche mit wenig Daten haben (um die Funktion des Systems zu testen), aber auch Massendaten. 

Zuerst erzeugen wir die Firmen (hier nur drei Stück) mit einer einzigen Anweisung. Das ist der erste wichtige Punkt, um eine gute Performance bei der Generierung der Daten zu erreichen. Natürlich könnte man auch drei INSERT-Statements nacheinander schreiben oder mit PL/SQL einen Loop 1..3 verwenden. Ab einer gewissen Zahl von Daten, die man erzeugen möchte, wird das aber massiv langsamer werden.

Teilabfrage „firma_seq“: Erzeugt eine Sequenz von 1..3 

Insert Statement: Die Firma soll einen aussagekräftigen Namen erhalten (hohe Nummer = viel Daten).

insert into firma (name) 
  select
    'Firma '||firma_seq.firma_nummer as name
  from (
    select rownum as firma_nummer 
    from dual 
    connect by level<=3
  ) firma_seq
;

Als nächstes möchten wir die Anlagen zu den Firmen erzeugen. Nicht vergessen: Wir wollen einige Bereiche mit wenig, andere mit viel Daten. Daher sollen zur ersten Firma drei Anlagen zugeordnet werden, zur zweiten Firma sechs Anlagen usw.

Teilabfrage „firma“: Durch das Ranking erhält man eine laufende Nummer 1..N, damit kann man auch einfach die gewünschte Anzahl Anlagen je Firma festlegen. Diese Abfrage produziert je Firma genau eine Zeile. 

Teilabfrage „anlage_seq“: Wir benötigen eine Sequenz, um durch den Join auf die „firma“ Teilabfrage je Firma und gewünschte Anzahl von Anlagen eine Zeile zu generieren. Dazu nehmen wir eine großzügig geschätzte Sequenz. Der Join wird durch die zuvor ermittelte Anzahl der Anlagen je Firma begrenzt. 

Insert Statement: Der Anlagenname soll erkennen lassen, zu welcher Firma diese Anlage gehört.  

insert into anlage (firma_id, name) 
  select 
    firma.firma_id,
    'Anlage '||firma.firma_nummer||'.'||anlage_seq.anlage_nummer
  from (
    select 
      firma_id, 
      dense_rank() over (order by firma_id) as firma_nummer,
      dense_rank() over (order by firma_id) * 3 as anzahl_anlagen
    from firma
  ) firma
  join (
    select rownum as anlage_nummer from dual connect by level<=1000
  ) anlage_seq on (anlage_seq.anlage_nummer <= firma.anzahl_anlagen)
;

Zuletzt soll noch je Firma und Anlage eine Abrechnung pro Kalendermonat generiert werden. Hier soll es weniger um die Joins gehen, stattdessen sollen Daten im Vordergrund stehen. 

Kernstück für zufällige Daten ist in Oracle das dbms_random Package. Es bietet mehrere Funktionen zur Erzeugung von Zahlen an, mit dem Standardbereich 0..1 oder mit selbst definierten Bereichen. Die Nachkommastellen der generierten Zahlen muss man allerdings selbst durch Rundung kontrollieren. Es steht ein Generator für Zeichen zur Verfügung, mit einigen Modi wie Groß, Klein, alphanumerisch, …  

Rechnungsnummer: Drei Großbuchstaben, gefolgt von sechs Ziffern 

Betrag: Ein zufälliger Wert im Bereich 100..900 Euro.  

Zuschlag: Bei ca. 10 % der Rechnungen soll ein Zuschlag von 50 € erhoben werden, das wird durch den Vergleich einer Zufallszahl im Bereich 0..1 auf < 0.10 erreicht. 

insert into abrechnung (firma_id,anlage_id,abrechnungsmonat,rechnungsnummer,betrag,zuschlag)
  select 
    firma.firma_id,
    anlage.anlage_id,
    to_date('01.'||monat||'.2021', 'dd.mm.yyyy') as abrechnungsmonat,
    dbms_random.string('U',3) || round(dbms_random.value(10000,99999), 0) as rechnungsnummer,
    round(100 + dbms_random.value(100,900),2) as betrag,
    case when dbms_random.value<0.10 then 50 else null end as zuschlag
  from firma
  join anlage 
    on anlage.firma_id = firma.firma_id
  join (
    select rownum as monat from dual connect by level<=12
  ) monat_seq on (1=1)
  order by 1,2,3
;

Fazit

Einen Schönheitspreis gewinnen diese Daten nicht, dafür sollte man besser auf die erwähnten Produkte zurückgreifen. Aber in unserem Projekt hat dieser Ansatz geholfen, vom ersten Tag an Daten in ausreichender Qualität und vor allem Quantität bereitzustellen. Damit konnten wir von Anfang an auch die Performance des Systems sicherstellen. Zudem kann im Endergebnis das System mehr Daten verarbeiten sowie diese auch schneller verarbeiten als ähnliche Systeme beim Kunden.


Source Code

--------------------------------------------------------------------------------------------------------------
-- Datenmodell anlegen
--------------------------------------------------------------------------------------------------------------
drop table abrechnung;
drop table anlage;
drop table firma;

create table firma (
  firma_id number(9) generated as identity,
  name varchar2(100) not null,
  constraint pk_firma primary key (firma_id)
);

create table anlage (
  anlage_id number(9) generated as identity,
  firma_id number(9) not null,
  name varchar2(100) not null,
  constraint pk_anlage primary key (anlage_id),
  constraint fk_anlage_01 foreign key (firma_id) references firma (firma_id)
);

create table abrechnung (
  abrechnung_id number(9) generated as identity,
  firma_id number(9) not null,
  anlage_id number(9) not null,
  abrechnungsmonat date not null,
  rechnungsnummer varchar(30) not null,
  betrag number(18,2) not null,
  zuschlag number(18,2),
  constraint pk_abrechnung primary key (abrechnung_id),
  constraint fk_abrechnung_01 foreign key (firma_id) references firma (firma_id),
  constraint fk_abrechnung_02 foreign key (anlage_id) references anlage (anlage_id)
);

--------------------------------------------------------------------------------------------------------------
-- Daten generieren
--------------------------------------------------------------------------------------------------------------

-- Alle Daten löschen

truncate table abrechnung;
truncate table anlage;
truncate table firma;

whenever sqlerror exit rollback;

-- Firmen erzeugen (hier 3)
insert into firma (name) 
  select
    'Firma '||firma_seq.firma_nummer as name
  from (select rownum as firma_nummer from dual connect by level<=3) firma_seq
;
 
commit;

-- Anlagen je Firma einfügen. Die erste Firma erhält drei Anagen, die nächste sechs Anlagen, ...
insert into anlage (firma_id, name) 
  select 
    firma.firma_id,
    'Anlage '||firma.firma_nummer||'.'||anlage_seq.anlage_nummer
  from (
    -- Je Firma deren laufende Nummer ermitteln und die gewünschte Anzahl von Anlagen
    select 
      firma_id, 
      dense_rank() over (order by firma_id) as firma_nummer,
      dense_rank() over (order by firma_id) * 3 as anzahl_anlagen
    from firma
  ) firma
  join (
    -- Eine Sequenz von Anlagen, durch den Join die benötigten Zeilen je Firma mit N Anlagen zu erhalten
    select rownum as anlage_nummer from dual connect by level<=1000
  ) anlage_seq on (anlage_seq.anlage_nummer <= firma.anzahl_anlagen)
  -- order by 1,2
;
 
commit;

-- Je Firma und Anlage eine Abrechnung je Kalendermonat erzeugen.
-- Die Rechnungsnummer ist ein zufälliger String mit drei Bucstaben und sechs Ziffern.
-- Der Betrag ist ein zufälliger Wert im Bereich 100..900 Euro.
-- Der Zuschlag von 50 Euro wird bei ca. 10% der Rechnungen gesetzt.
insert into abrechnung (firma_id,anlage_id,abrechnungsmonat,rechnungsnummer,betrag,zuschlag)
  select 
    firma.firma_id,
    anlage.anlage_id,
    to_date('01.'||monat||'.2021', 'dd.mm.yyyy') as abrechnungsmonat,
    dbms_random.string('U',3) || round(dbms_random.value(10000,99999), 0) as rechnungsnummer,
    round(dbms_random.value(100,1000),2) as betrag,
    case when dbms_random.value<0.10 then 50 else null end as zuschlag
  from firma
  join anlage on anlage.firma_id = firma.firma_id
  join (select rownum as monat from dual connect by level<=12) monat_seq on (1=1)
  -- order by 1,2,3
;
 
commit;

Verteiltes Arbeiten – Ein Erfahrungsbericht zur praktischen Anwendung von Remote Mob Testing

Innerhalb von ZEISS Digital Innovation (ZDI) gibt es in regelmäßigen Abständen eine interne Weiterbildungsveranstaltung – den sogenannten ZDI Campus. Dabei präsentieren wir als Mitarbeiterinnen und Mitarbeiter Softwareentwicklungsthemen mittels Vorträgen, Workshops oder Diskussionsrunden. 

Katharina hatte beim vergangenen ZDI Campus schon einmal über kollaborative Testmethoden berichtet und auch Blogartikel bezüglich Remote Pair Testing sowie Remote Mob Testing veröffentlicht. Daher wollten wir das Thema gern auf dem nächsten ZDI Campus weiterführen und einen Workshop aus der Themenwelt kollaborativer Testmethoden anbieten. Aufgrund der Covid-19-Pandemie musste der nächste Campus jedoch online stattfinden. Dennoch wollten wir mehreren Teilnehmerinnen und Teilnehmern die Möglichkeit bieten, eine Testmethode am praktischen Beispiel anzuwenden und haben uns daher für einen remote Mob Testing Workshop entschieden.  

Doch es gab eine Herausforderung: Wir hatten noch nie mit so vielen Personen im Mob remote gearbeitet! 

Aufbau des Workshops 

Wie in dem Blogbeitrag über Remote Mob Testing beschrieben ist es sinnvoller, kleine Gruppen aus vier bis sechs Personen zu betreuen. Durch das verteilte Arbeiten kann es sonst häufiger zu Verzögerungen durch technische Probleme (z. B. schlechte Verbindung, schlechte Tonqualität) kommen, was die Einsatzzeit des jeweils aktuellen Navigators verkürzen könnte. Auch kann man als Facilitator bei kleineren Gruppen besser den Überblick behalten und die Teilnehmenden können die Rolle des Navigators oder Drivers häufiger einnehmen.
(Zur Erläuterung der verschiedenen Rollen siehe ebenfalls Blogbeitrag Remote Mob Testing

Unser Setting sah wie folgt aus:

  • Microsoft Teams als Kommunikationsplattform  
  • Leicht verständliches Testobjekt (Website)
  • 3 Facilitators  
  • 39 Teilnehmerinnen und Teilnehmer aus den Bereichen QA, Entwicklung und Business Analyse 
  • Zeitlicher Rahmen: 1,5 h  

Weil wir allen die Möglichkeit geben wollten, das Mob Testing selbst an einem praktischen Beispiel kennenzulernen, haben wir im Vorfeld des Workshops keine Teilnehmerbegrenzung festgelegt. Schlussendlich hatten alle drei Facilitators über 12 Teilnehmerinnen und Teilnehmer. 

Person am Schreibtisch, in einer Videokonferenz mit vielen weiteren Personen
Beim Remote Mob Testing nehmen alle Teilnehmenden einmal die aktive Rolle des Navigators ein.

Als Testobjekt haben wir eine einfache Website gewählt, damit sich alle auf das Vermitteln bzw. Kennenlernen der Testmethode konzentrieren konnten und sich nicht noch zusätzlich Wissen über die Anwendung aneignen mussten. 

Feedback 

Schon während der Durchführung des Workshops fiel uns auf, dass die Wartezeiten, bis eine aktive Rolle (Driver oder Navigator) eingenommen wird, als unangenehm empfunden werden könnte.   

Dies wurde auch in der Feedbackrunde angesprochen. Daher empfehlen wir, dass der Mob bei Testideen mit unterstützt. Es bedeutet nämlich nicht, dass man sich als Mob-Mitglied zurücklehnen und mit den Gedanken abschweifen kann. Das vermeidet auch doppeltes Testen oder sich die Blöße geben zu müssen, nicht aufgepasst zu haben.  

Teilweise fiel es einigen Teilnehmenden am Anfang schwer, sich bei der Rolle des Drivers darauf zu konzentrieren, nur die Anweisungen des Navigators auszuführen und eigene Testideen zurückzuhalten. Durch entsprechende Hinweise des Facilitators gewöhnten sich die Teilnehmerinnen und Teilnehmer jedoch nach einiger Zeit daran. Dieser Aspekt des Mob Testings wurde als sehr positiv empfunden, weil alle in der Rolle des Navigators zu Wort kommen und eigene Testideen einbringen können.  

Dennoch kam die Frage auf, warum die Rollen Navigator und Driver nicht zusammengefasst werden können. Dazu lässt sich Folgendes sagen: Es fördert den Lernprozess, wenn ein Mitglied Schritt für Schritt artikuliert, was es vorhat. So bindet man mehr Teilnehmerinnen und Teilnehmer in diese aktive Rolle ein. Durch das Mitteilen des Ziels wird es den Teilnehmenden erleichtert, den Ideen des Navigators zu folgen. Andernfalls kann es passieren, dass einige Schritte zu schnell und mit zu wenig Erklärung durchgeführt werden. Dadurch ginge die Nachvollziehbarkeit verloren und dem Mob würde es erschwert, sich aktiv einzubringen.  

Weiteres positives Feedback gab es zur Rolleneinteilung und dem gesamten Vorgehen. Den Aussagen der Befragten nach erhalte man die Testansätze zum Teil durch den Weg, den der vorherige Navigator eingeschlagen hat und betrachtet daher das Testobjekt aus den unterschiedlichsten Blickwinkeln. Aus diesem Grund ist es immer sinnvoll – je nach Zweck der Mob Testing Session – Teilnehmerinnen und Teilnehmer mit unterschiedlichen Kenntnissen einzuladen. Das erhöht den Lernprozess und die Anzahl der Testfälle. Das einfache Beispiel-Testobjekt habe zudem geholfen, sich auf die Methode zu konzentrieren und sie zu verinnerlichen. 

Sehr positiv hervorgehoben wurde das kollaborative Arbeiten beim Mob Testing. Dadurch wird der agile Gedanke gelebt.  

Lösungsansatz für eine große Teilnehmerzahl 

Das Problem einer zu hohen Teilnehmeranzahl könnte man lösen, indem man im Vorfeld die Gruppengröße begrenzt oder aber eine neue Rolle einführt: die Rolle des Zuschauers. Dabei würde dann eine Trennung zwischen aktiven Teilnehmern und Teilnehmerinnen einerseits sowie Zuschauerinnen und Zuschauern andererseits vollzogen. Die Teilnehmenden würden das oben beschriebene Vorgehen durchführen und sich an die Rollenverteilung (Navigator, Driver, Mob) sowie den Rollenwechsel halten. Die Zuschauerinnen und Zuschauer würden nur beobachten und nicht teilnehmen. Auch Kommentare von ihnen wären nicht erlaubt, weil das bei einer hohen Zuschauerzahl die aktiven Teilnehmenden stören könnte. 

Fazit 

Alles in Allem ist der Workshop auf der Campus-Veranstaltung sehr gut angekommen und hat gezeigt, dass es sehr gut möglich ist, Mob Testing auch remote und somit für das verteilte Arbeiten anzuwenden. Durch diese Möglichkeit kann das Zusammenarbeiten beibehalten werden, auch wenn es nicht immer möglich ist, sich vor Ort zu treffen.  

Dieser Beitrag wurde verfasst von:

Maria Petzold

Maria Petzold arbeitet seit 2010 bei der ZEISS Digital Innovation. Als Testmanagerin liegt ihr Fokus auf der Qualitätssicherung von Software. Vor allem in medizinischen Softwareprojekten konnte sie ihre Test-Erfahrungen sammeln.

Alle Beiträge des Autors anzeigen