Java Magazin   3.2019 - Chaos Engineering

Erhältlich ab:  Februar 2019

Autoren / Autorinnen: 
Sebastian Daschner ,  
Tam Hanna ,  
Konstantin Diener ,  
Steffen GrunwaldSascha Möllering ,  
,  
Sebastian Meyen ,  
Lars Gohr ,  
Benjamin Wilms ,  
Manfred Steyer ,  
Gernot StarkePeter Hruschka ,  
Benjamin Wilms ,  
Marc Philipp ,  
Frank RothGerd Herbertz ,  
Markus Günther ,  
Pierre Gronau ,  
Lars Röwekamp

Das MusicStore-Team sitzt beim Mittagessen zusammen. Martin hat am Vorabend an einem Meetup teilgenommen und berichtet seinen Kollegen von den neuen Erkenntnissen, die er dort gewonnen hat.

Martin: „Ich war gestern Abend das erste Mal auf dem Sec Meetup.“

Lukas: „Cool, ist das dieses Security Meetup, von dem du erzählt hast? Wie bist du darauf gekommen?“

Martin: „Das Meetup fand in der Firma statt, in der mein Kumpel Rüdiger arbeitet. Sehr coole Location ...“

Lukas: „Ich hab’s gestern leider nicht geschafft. War’s denn interessant?“

Martin: „Auf jeden Fall. Es ging um ‚DevSecOps – Für Security ist jeder verantwortlich‘. Ich habe nur gemerkt, dass ich mir solche Themen nicht abends anhören kann. Es hat mich so intensiv zum Grübeln gebracht, dass ich noch zwei Stunden wach gelegen habe.“

Lukas: „Wow. Was hat dich so beschäftigt?“

Martin: „Worauf man alles achten muss, wenn man halbwegs sichere Software bauen möchte – und was man alles falsch machen kann. Wir haben da ganz schön Nachholbedarf!“

Christian: „Wieso? Security kaspert Erik doch mit Gunnar, unserem CISO, aus.“

Martin: „Es ging gestern auch um einige Themen, die sich auf die Zusammenarbeit mit dem PO bezogen. Wusstet ihr aber, dass es zum Beispiel ein Tool gibt, mit dem man Maven Dependencies auf Schwachstellen scannen kann? Oder eins, mit dem sich Angriffspunkte für SQL Injections in der Anwendung aufdecken lassen? Das sehe ich auch eher als Aufgabe der Entwickler. Da hat Erik nichts mit zu tun.“

Lukas: „Außerdem, Christian, haben wir uns in letzter Zeit auch nicht mit Ruhm bekleckert, wenn Erik sicherheitsrelevante Dinge eingekippt hat. Ich erinnere nur an die Testumgebung, die immer noch offen im Netz rumsteht ...“

Christian: „Was soll da auch groß passieren, ehrlich?“

Martin: „Oh, da habe ich gestern die eine oder andere Horrorstory mit Data Leaks gehört. Das würde ich spätestens seit gestern nicht mehr so auf die leichte Schulter nehmen, Christian. Vor allem, weil die Daten ja unverschlüsselt auf der Umgebung liegen.“

diener_devops_1.tif_fmt1.jpgAbb. 1: Martin berichtet seinen Kollegen beim Mittagessen vom gestrigen Security Meetup

Security wird oft als lästiges Nice-to-have gesehen

Martin ist nach dem Meetup überzeugt: Jeder ist für Security verantwortlich. Christian sieht das ganz anders. Für ihn ist Security ein lästiges Thema, um das sich der Chief Information Security Officer (CISO) zusammen mit dem Product Owner kümmern soll. Vermutlich gibt es bei jedem Go Live einen Schritt, in dem Gunnar, der CISO, noch ein paar Securitytests durchführt.

Die ablehnende Haltung der Entwickler kommt nicht von ungefähr. Oft wird Security auch im Management als kostspieliges Nice-to-have gesehen. Das Thema wird erst interessant, wenn das erste Sicherheitsproblem aufgetaucht ist. Bis dahin wird Security auf einen kurzen Checklisteneintrag vor dem Go Live reduziert [1].

Mit der agilen Softwareentwicklung zog ein konsequentes Qualitätsmantra in die Entwicklungsteams ein. Waren Tests zuvor ein manueller Schritt vor der Inbetriebnahme, gibt es nun eine Vielzahl unterschiedlicher Tests in jedem Stadium des Softwareentwicklungsprozesses. Diese Tests werden bereits im Sprint Planning beim Schreiben von User Stories vorbereitet. DevOps-Teams messen zudem kontinuierlich den Gesundheitszustand ihrer Anwendung in Produktion.

Genau wie beim Thema Qualität ist auch für Security ein Kulturwandel notwendig.

Entwicklungsteams müssen eine ebenso positive Einstellung zum Thema Security wie zur Softwarequalität entwickeln [2]. Es ist klar, dass keine Organisation jemals hundertprozentige Security erreichen wird, noch passiert aber oft zu wenig.

Werner Vogels hat in einem Blogpost ein Resümee über zehn Jahre AWS geschrieben [3]. Bemerkenswert ist, dass in diesem Text mindestens drei von zehn Punkten mit Security zu tun haben: 2. Expect the unexpected, 7. Build security in from the ground up, 8. Encryption is a first-class citizen.

Security fängt beim Sprint Planning an ...

Den ersten Punkt „Expect the unexpected“ bezieht er eigentlich darauf, dass in einem verteilten System jede Komponente jederzeit ausfallen kann. Dieses Mantra hilft uns aber auch bei der Planung von Security (dem sogenannten „Whiteboard Hacking“) weiter. Jedes vom Benutzer übertragene Datum ist potenziell gefährlich und kann SQL Injections oder ähnlichen Schadcode enthalten. Bei diesen Daten muss es sich nicht unbedingt um direkte Eingaben in einer Webanwendung handeln. Es können beispielsweise auch manipulierte Metadaten in einer Datei sein. Gojko Adzic beschreibt in seinem Buch „Humans vs. Computers“ [4] einige Szenarien, die beschreiben, was in diesem Zusammenhang alles schiefgehen kann.

Ein nützliches Werkzeug für das Whiteboard Hacking sind Evil User Stories [5]. Herkömmliche User Stories beschreiben das Verhalten von normalen beziehungsweise wohlmeinenden Benutzern. Evil User Stories versetzen sich in einen möglichen Angreifer hinein und beschreiben seine Versuche, die Anwendung zu kompromittieren. Diese Stories kann das Team gemeinsam mit seinem Product Owner schreiben – möglicherweise unterstützt von Securityexperten – und der PO kann sie wie alle anderen Stories im Backlog priorisieren.

... ist fester Bestandteil der Softwareentwicklung ...

Durch Whiteboard Hacking lassen sich potenzielle Schwachstellen im Rahmen der Planung aufdecken. Während der Entwicklung ist eine konsequente Berücksichtigung ebenso wichtig – dies kann zum Beispiel in Form von Security Code Reviews geschehen. Zusätzlich gibt es eine ganze Reihe mächtiger und zum Teil kostenloser Werkzeuge, die bei Penetrationstests oder beim Aufdecken von Sicherheitslücken unterstützen.

Entwicklungsteams machen heute in großem Umfang Gebrauch von Open-Source-Bibliotheken und schreiben nur noch einen Teil ihrer Software selbst. Diese Bibliotheken enthalten ebenfalls immer wieder Schwachstellen, die ein Angreifer sich zunutze machen kann. Martin berichtet seinen Kollegen von einem Werkzeug, das Maven Dependencies mit einer Liste bekannter Schwachstellen abgleicht (OWASP Dependency Check [6], [7]). Solche Werkzeuge helfen im Entwicklungsprozess, Schwachstellen in den verwendeten Bibliotheken zügig zu identifizieren und entsprechend darauf zu reagieren (Update der Bibliothek oder Anpassung des aufrufenden Codes).

Laut den „OWASP Top 10“ [8] gehört die Verwendung von Komponenten mit bekannten Schwachstellen zu den zehn häufigsten Securityproblemen. Für kleine Teams stellt es eine besondere Herausforderung dar, alle Infrastrukturkomponenten auf einem aktuellen Patch Level zu halten. Solche Teams entscheiden sich häufig, Managed Components einzusetzen und damit die Verantwortung für die Security und das Patchen der Infrastrukturkomponenten zum Beispiel an einen Cloud-Anbieter zu delegieren [9].

Die Verwendung von Managed Services entbindet das Team allerdings nicht davon, bei seiner Architektur konsequent auf verschlüsselte Kommunikation und Speicherung sensibler Daten zu achten.

... und des Betriebs

Außerdem fallen, unabhängig davon, ob ausschließlich Managed Services verwendet werden oder nicht, in jedem Fall eine Reihe von Securityaufgaben im Betrieb der Anwendung an. Ebenso wie DevOps-Teams den Gesundheitszustand ihrer Anwendung kontinuierlich messen, sollte es auch ein entsprechendes Security Logging und Monitoring geben (fehlgeschlagene Anmeldeversuche, versuchte SQL Injections etc.), um Angreifer aussperren oder zumindest einen Angriff nachverfolgen zu können. Laut den „OWASP Top 10“ gehört auch unzureichendes Logging und Monitoring zu den zehn häufigsten Securityproblemen. Aus diesem Grund tauchen diese Themen gleich zweimal im Manifesto der DevSecOps-Initiative [10] auf.

Jedes Unternehmen sollte Securityexpertise haben, nicht zwingend jeder Mitarbeiter

Der bisherige Artikel enthielt nur eine kleine Auswahl von Themen, die im DevSecOps-Kontext relevant sind. Auch diese Auswahl zeigt, dass das Thema sehr vielschichtig und umfassend ist. Aus diesem Grund muss die Regel „Jeder ist für Security verantwortlich“ nicht bedeuten, dass alle Mitarbeiter auch Securityexperten sind – genau wie in einem DevOps-Team auch nicht alle Teammitglieder Infrastrukturexperten sind. Alle Mitarbeiter sollten jedoch für dieses Thema sensibilisiert und lernwillig sein und darüber hinaus sollte es noch Securityexperten in den einzelnen Teams oder in einem separaten Team geben. Diese Experten können dann zum Beispiel Tools für den Softwareentwicklungsprozess und fürs Monitoring entwickeln und den Kollegen zur Verfügung stellen.

Martin hat mit Kollegen aus anderen Teams eine Community of Practice [11] ins Leben gerufen, in der sie entsprechende Werkzeuge und Prozeduren für ihr Unternehmen entwickeln möchten. Im Sprint Planning wollen er und sein Team sich zukünftig zusammen mit Erik und Gunnar auch mit Evil User Stories beschäftigen.

diener_konstantin_sw.tif_fmt1.jpgKonstantin Diener ist CTO bei cosee. Mit dem Thema Security geht er ähnlich um wie Martin. Er hat eine Community of Practice ins Leben gerufen, in der sich Kollegen aus verschiedenen Teams regelmäßig zu Securitythemen austauschen.

Web Twitter

„Chaos“ klingt ja nach etwas, das wirklich keiner will oder brauchen kann. Und da kommen schon die Software-Hipster und predigen das neue „Chaos Engineering“ …

Chaos zu stiften ist jedoch weder Zweck noch Ziel, sondern ein Mittel, komplexe Systeme in kontrollierter Art und Weise auf Herz und Nieren zu testen. Einen solchen Ansatz benötigen wir noch nicht unbedingt, wenn wir es mit Systemen zu tun haben, die im klassischen Sinn „kompliziert“ sind. Solche Systeme zeichnen sich dadurch aus, dass es bei Ereignissen klare Ursache-Wirkung-Relationen zu erkennen gibt, die man analysieren, dokumentieren und vorhersagen kann. Bei „komplexer“ Software ist davon auszugehen, dass Fachleute deren innere Mechanismen hinreichend kennen und im Idealfall Fehler komplett vermeiden.

Geht in einem solchen Umfeld doch mal etwas schief, ist dies auf menschliche Fehler, mangelhafte Dokumentation oder etwa die Unübersichtlichkeit des Systems zurückzuführen. Die klassische Optimierungsstrategie lautet meistens: Kontrolle erhöhen, damit bekommt man schon alles in den Griff.

Komplexe Systeme hingegen sind solche, bei denen die Beziehung zwischen Ursache und Wirkung von Ereignissen nur im Nachhinein und auch nicht mit absoluter Sicherheit wahrgenommen werden kann.

In solchen Umgebungen ist nicht Fehlerfreiheit die Königstugend der Softwareentwickler, sondern die sog. Mean Time to Recover (MTTR). In anderen Worten: Das Auftreten eines Fehlers bedeutet keinen Skandal, den es zu vermeiden gilt; Fehler gehören vielmehr zum normalen Gang der Dinge, und es kommt darauf an, ob wir in der Lage sind, sie schnell zu analysieren und ebenso schnell pragmatische Lösungen für deren Behebung umzusetzen.

Warum man also an komplexe Systeme herangehen und mit einer klaren Hypothese ausgestattet Durcheinander stiften sollte, erfahren Sie im ersten Teil des von Benjamin Wilms verfassten Titelthemas dieser Ausgabe (Seite 58). Im zweiten Teil geht der Autor dann auf konkrete Werkzeuge und Vorgehensweisen für erste konstruktive Chaos-Experimente ein (Seite 66).

Bemerkenswert an dem Thema ist, dass es nicht nur darum geht, komplexe technische Abhängigkeitsstrukturen ausfindig zu machen und für den Fehlerfall frühzeitig Reparaturstrategien zu entwickeln, sondern auch um den menschlichen Faktor.

In diesem Sinne ließe sich Chaos Engineering mit einer Feuerwehrübung oder anderen Notfallszenarien in der analogen Welt vergleichen: Selbst wenn alle Prozesse bis ins Kleinste definiert, selbst wenn alle Rollen umfassend erprobt und klar zugewiesen sind – erst im Zusammenspiel aller Faktoren und unter simulierten Realbedingungen beginnt man zu verstehen, was sich wirklich in einem komplexen System ereignet.

meyen_sebastian_sw.tif_fmt1.jpgSebastian Meyen | Chefredakteur

Mail Website Twitter Xing

Im September 2017 wurde die Version 5.0 von JUnit nach fast zweijähriger Entwicklung endlich fertiggestellt. Das JUnit-Team hat die Arbeit danach jedoch keineswegs eingestellt, sondern neben einigen Bugfix-Releases mittlerweile drei neue Featurereleases veröffentlicht.

Nach einer kurzen Einführung in das neue Programmiermodell für Tests konzentrieren wir uns in diesem Artikel auf die Neuerungen in den Featurereleases (zum Beispiel Kotlin-Support, Tag Expressions und parallele Testausführung) und lernen, dass JUnit 5 viel mehr ist, als ein Testing-Framework für Java.

Das neue Programmiermodell

Das mit JUnit 5.0 vorgestellte neue Programmiermodell zum Schreiben von Tests und Erweiterungen trägt den Namen JUnit Jupiter API. Da das JUnit-Team bezweckt, dieses API über mehrere Major-Versionen hinweg weiterzuentwickeln, enthält der Name bewusst keine Versionsnummer. Eine kleine Referenz zur Zahl Fünf gibt es dennoch, denn Jupiter ist, von der Sonne aus gezählt, der fünfte Planet in unserem Sonnensystem.

Listing 1: Eine erste Testklasse

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
 
class CalculatorTests {
 
  private Calculator calculator;
 
  @BeforeEach
  void createCalculator() {
    calculator = new Calculator();
  }
 
  @Test
  @DisplayName("1 + 1 = 2")
  @Tag("addition")
  void onePlusOneIsTwo() {
    long newValue = calculator.set(1).add(1).longValue();
 
    assertEquals(2, newValue);
  }
 
  @Test
  @DisplayName("(2 * 3) / 4 = 6/4 = 3/2 = 1.5")
  @Tag("multiplication")
  @Tag("division")
  void divideResultOfMultiplication() {
    BigDecimal newValue = calculator.set(2).multiply(3).divide(4).get();
 
    assertEquals(new BigDecimal("1.5"), newValue);
  }
 
  @Test
  @Tag("input-validation")
  void cannotSetValueToNull() {
    IllegalArgumentException exception
      = assertThrows(IllegalArgumentException.class, () -> calculator.set(null));
 
    assertEquals("cannot set value to null", exception.getMessage());
  }
 
}

Am besten lässt sich das neue Programmiermodell anhand eines Beispiels erklären. Dazu betrachten wir Unit -Tests für eine Calculator-Klasse, die einen einfachen Taschenrechner repräsentiert (Listing 1). Eine Calculator-Instanz hat jederzeit einen aktuellen Wert, der durch Anwendung einer Rechenoperation, d. h. den Aufruf einer Methode, modifiziert wird. Alle Codebeispiele in diesem Artikel sind auf GitHub verfügbar [1].

Auf den ersten Blick sieht die Testklasse sehr ähnlich aus wie eine, die auf JUnit 4 basiert. So werden Testmethoden weiterhin mit @Test annotiert. Auf den zweiten Blick fällt allerdings auf, dass die Annotationen nun aus dem org.junit.jupiter.api-Package und nicht aus org.junit importiert werden. Des Weiteren müssen Testklassen und -methoden in Jupiter nicht mehr public sein, die Standardsichtbarkeit genügt.

Wie man anhand der createCalculator()-Methode sehen kann, gibt es weiterhin eigene Annotationen für Methoden der Testklasse, die am Lebenszyklus eines Tests teilnehmen möchten. Allerdings haben sie leicht andere Namen als in JUnit 4 (zum Beispiel @BeforeEach statt @Before).

philipp_junit_1.tif_fmt1.jpgAbb. 1: Ausführung von „CalculatorTests“ in IntelliJ IDEA

Wie die onePlusOneIsTwo()-Methode zeigt, ist man beim Benennen der Tests nun nicht mehr auf die Syntax von Methodennamen in Java beschränkt, sondern kann mit Hilfe der @DisplayName-Annotation beliebige Zeichenketten verwenden. Dennoch kann man in IDEs direkt von dem Eintrag im Testbaum (Abb. 1) zur Methode navigieren.

Das Jupiter API bietet mit der Assertions-Klasse eine Basismenge an Assertions an, die so manchem (wie etwa assertEquals()) ebenfalls von JUnit 4 bekannt vorkommen dürfte. Wie man in der cannotSetValueToNull()-Methode sieht, gibt es darüber hinaus aber auch neue Assertions. Mit assertThrows() lässt sich zusichern, dass ein bestimmter Codeabschnitt (hier calculator.set(null)) eine gewisse Art von Exception wirft (hier IllegalArgumentException). Der Rückgabewert ist die tatsächlich aufgetretene Exception und kann verwendet werden, um weitere Assertions an Eigenschaften (hier die erwartete Message) zu formulieren. Das JUnit-Team empfiehlt Entwicklern, denen die in der Assertions-Klasse enthaltenen Methoden nicht ausreichen, ausdrücklich, Bibliotheken wie AssertJ [2] oder Hamcrest [3] zu verwenden.

Anstelle von Categories bietet Jupiter @Tags, die nun ebenfalls auf Strings basieren. Wie wir später sehen werden, sind Tags nützlich zur Filterung der auszuführenden Tests.

Parametrisierte Tests

Neben klassischen Testmethoden, die üblicherweise das Resultat einer beispielhaften Eingabe gegen eine erwartete Ausgabe prüfen, bietet das JUnit Jupiter API weitere Möglichkeiten, Tests zu formulieren. Diese sind insbesondere dann hilfreich, wenn man denselben Code mit vielen verschiedenen Eingaben testen möchte. Eine solche Variante stellen parametrisierte Tests dar.

Listing 2: Parametrisierter Test

@ParameterizedTest(name = "sqrt({0}) = {1}")
@CsvSource({
  "1, 1.0000000000000000",
  "2, 1.4142135623730951",
  "3, 1.7320508075688772",
  "4, 2.0000000000000000"
})
@Tag("sqrt")
void sqrt(long input, double expectedResult) {
  double actualResult = calculator.set(input).sqrt().doubleValue();
 
  assertEquals(expectedResult, actualResult, 1e-16);
}

Um einen parametrisierten Test zu schreiben, annotiert man die Testmethode mit @ParameterizedTest anstelle von @Test, wie etwa an der sqrt()-Methode im Beispiel (Listing 2). Zusätzlich muss man mindestens eine Source-Annotation deklarieren, die angibt, woher die Werte für die Parameter gelesen werden sollen. Im Beispiel verwenden wir @CsvSource und definieren die Parameter als kommagetrennte Liste. JUnit konvertiert die Strings automatisch in die deklarierten Parametertypen. Die Konvertierung lässt sich über eine zusätzliche @ConvertWith-Annotation anpassen. Details dazu findet man im JUnit 5 User Guide [4].

Die Testmethode wird einmal pro Parameterliste ausgeführt. Jede Ausführung weist den gleichen Lebenszyklus wie eine normale Testmethode auf, die mit @Test annotiert ist. So werden etwa @BeforeEach/@AfterEach-Methoden vor bzw. nach jeder Ausführung aufgerufen.

Ein Vorteil von @CsvSource ist direkte Sichtbarkeit der Testdaten an der Testmethode. Allerdings kann die Definition der Daten unübersichtlich werden, je mehr Zeilen und Spalten man definiert. Manchmal möchte man außerdem dieselben Daten als Eingabeparameter für mehrere Tests verwenden. Ersetzt man die @CsvSource-Annotation durch @CsvFileSource(resources = "/sqrt.csv") und fügt sqrt.csv (Listing 3) zu src/test/resources hinzu, lässt sich genau das erreichen.

Listing 3: „sqrt.csv“

1, 1.0000000000000000
2, 1.4142135623730951
3, 1.7320508075688772
4, 2.0000000000000000

Neben @CsvSource und @CsvFileSource gibt es noch viele weitere Möglichkeiten, die Quelle der Testdaten anzugeben. Am einfachsten ist mit Sicherheit die @ValueSource-Annotation, die es erlaubt, ein eindimensionales Array von primitiven Typen, Strings oder Class-Literalen zu verwenden. Des Weiteren unterstützt @EnumSource die Verwendung von Enum-Konstanten und @MethodSource die Definition der Testdaten in einer statischen Methode der Testklasse oder  – zwecks Wiederverwendung in verschiedenen Testklassen – einer beliebigen anderen Klasse. Darüber hinaus kann man eigene ArgumentsProvider implementieren und per @ArgumentsSource oder über eine eigene Annotation verwenden.

Dynamische Tests

Testklassen und -methoden sind insofern statisch, als die darin enthaltenen Methoden bekannt und unveränderlich sind, sobald die Klasse kompiliert wurde. Im Gegensatz dazu bieten dynamische Tests die Möglichkeit, Testfälle zur Laufzeit programmatisch zu erzeugen.

Listing 4: Dynamische Tests

@TestFactory
@Tag("multiplication")
@Tag("power")
Stream<DynamicTest> powersOfTwo() {
  return IntStream.range(1, 100)
    .mapToObj(value -> dynamicTest(MessageFormat.format("{0}^2 = {0} * {0}", value), () -> {
      var expectedValue = new Calculator(value).multiply(value).get();
      var actualValue = calculator.set(value).power(2).get();
      assertEquals(expectedValue, actualValue);
    }));
}

Wie die powersOfTwo()-Methode (Listing 4) beispielhaft zeigt, annotiert man dazu eine Methode einer Testklasse mit @TestFactory anstelle von @Test und definiert den Rückgabewert als Stream<DynamicTest> anstelle von void (siehe JUnit User Guide für weitere unterstützte Rückgabetypen). Bei der Implementierung der Methode stehen nun alle Sprachmittel von Java zur Verfügung. Im Beispiel nehmen wir alle Zahlen von 1 bis 99 und erzeugen daraus jeweils eine Instanz von DynamicTest mittels der statisch importierten dynamicTest()-Methode. Der erste Parameter ist dabei der Anzeigename des Tests, der zweite der auszuführende Codeblock (entspricht dem Body einer regulären Testmethode). Führt man die Methode aus, wird pro DynamicTest ein Eintrag im Testbaum angezeigt (Abb. 2). So lassen sich schnell feingranulare Assertions definieren, deren Ergebnisse detailliert angezeigt werden.

philipp_junit_2.tif_fmt1.jpgAbb. 2: Ausführung von „CalculatorTests.powerOfTwo()“ in IntelliJ IDEA

Repeated Tests

Zu guter Letzt bietet das neue Programmiermodell die Möglichkeit, Tests mehrfach auszuführen. Das ist etwa dann hilfreich, wenn ein Test flaky ist, d. h. manchmal erfolgreich durchläuft und andere Male, ohne Änderungen an Test oder Implementierung, fehlschlägt. Ersetzt man die @Test-Annotation durch @RepeatedTest, wird der Test entsprechend der angegebenen Anzahl wiederholt (Listing 5). So kann man schnell validieren, ob ein Test flaky ist, ohne dass die verwendete IDE-Mehrfachausführung als eigenes Feature unterstützen muss.

Listing 5: Wiederholter Test

@RepeatedTest(10)
@Tag("power")
void flakyTest() {
  double actualResult = calculator.set(Math.random()).power(2).doubleValue();
 
  assertEquals(0.0, actualResult, 0.5);
}

Extensions

Automatisierte Tests sind heutzutage glücklicherweise essenzieller Bestandteil moderner Softwareentwicklung. Auch beim Schreiben von Testcode wollen wir uns nicht ständig wiederholen und wartbar soll das Resultat schlussendlich auch noch sein. Damit die Tests möglichst prägnant beschreiben, was wir eigentlich testen wollen, ist es daher eminent wichtig, wie gut ein Testing-Framework uns dabei unterstützt, häufig verwendetes Set-up/Teardown wiederzuverwenden und unnötigen Code zu vermeiden. Das JUnit-Team hat sich beim Design des Jupiter APIs sehr viele Gedanken über diese Art von Erweiterbarkeit gemacht. Während es in JUnit 4 mit @RunWith und @Rule zwei konkurrierende Erweiterungsmechanismen gibt, enthält das Jupiter API mit dem Extension API ein einziges durchgängiges Konzept.

Das Extension-Interface ist ein Marker-Interface und wird von mittlerweile elf konkreten Interfaces erweitert. Eine konkrete Extension-Implementierung kann dabei so viele dieser Interfaces implementieren wie gewünscht. Leider fehlt uns in diesem Artikel der Raum, auf jeden der verfügbaren Extension Points einzugehen. Stattdessen sehen wir uns am Beispiel der mittlerweile offiziellen Extension von Mockito [5], einer Bibliothek zum Erzeugen von Mock-Objekten, an, wie eine Extension registriert wird und was sie leisten kann (Listing 6). Die Registrierung der MockitoExtension erfolgt mittels der @ExtendWith-Annotation an einer Testklasse oder -methode. Da die MockitoExtension das ParameterResolver-Interface implementiert, das eine Erweiterung von Extension ist, wird sie von JUnit zum Ermitteln der Parameter der Testmethode verwendet. Mockito erzeugt Mock-Objekte für alle mit @Mock annotierten Parameter. In unserem Beispiel wird das Mock-Objekt dann im Test verwendet, um die Klasse CurrencyConverter zu testen, die einen ExchangeRateService benötigt.

Listing 6: Test, der die „MockitoExtension“ verwendet

@ExtendWith(MockitoExtension.class)
class CurrencyConverterTests {
 
  private static final Currency EUR = Currency.getInstance("EUR");
  private static final Currency USD = Currency.getInstance("USD");
 
  @Test
  void convertsEurToUsd(@Mock ExchangeRateService exchangeRateService) {
    var originalAmount = new MonetaryAmount("100.00", EUR);
    when(exchangeRateService.getRate("EUR", "USD")).thenReturn(1.139157);
 
    var currencyConverter = new CurrencyConverter(exchangeRateService);
    var convertedAmount = currencyConverter.convert(originalAmount, USD);
 
    assertEquals(new BigDecimal("113.92"), convertedAmount.getValue());
    assertEquals(USD, convertedAmount.getCurrency());
  }
}

Neben der deklarativen Registrierung von Extensions mittels @ExtendWith bietet JUnit außerdem noch die Möglichkeit der programmatischen Registrierung über ein mit @RegisterExtension annotiertes Feld der Testklasse. Alternativ können Extensions sogar global für alle Tests registriert werden. Details dazu findet man im JUnit User Guide.

Neben Mockito gibt es mittlerweile eine ganze Reihe an fertigen Extensions, die man zum Schreiben von Tests verwenden kann, zum Beispiel für Spring, Testcontainers, Wiremock, Kafka und viele mehr. Das JUnit-Team pflegt eine Liste von Extensions im Wiki auf GitHub [6].

Unterstützung für Kotlin

Die Programmiersprache Kotlin ist zurzeit in aller Munde und selbstverständlich gibt es Testframeworks, die speziell dafür geschrieben wurden. Aber auch das Jupiter API lässt sich sehr gut in Kotlin verwenden. Ein wesentlicher Vorteil besteht darin, dass sich bestehende Extensions, wie etwa für Spring oder Testcontainers, so weiterhin verwenden lassen.

Listing 7: Testklasse in Kotlin

@TestInstance(Lifecycle.PER_CLASS)
class KotlinCalculatorTests {
 
  private lateinit var calculator: Calculator
 
  @BeforeAll
  fun createCalculator() {
    calculator = Calculator()
  }
 
  @Test
  @Tag("input-validation")
  fun `cannot set value to null`() {
    val exception = assertThrows<IllegalArgumentException> { calculator.set(null) }
 
    assertAll(
      { assertEquals("cannot set value to null", exception.message) },
      { assertNull(exception.cause) }
    )
  }
}

Speziell für das Schreiben von Tests mit Kotlin hat das JUnit-Team in Version 5.1 begonnen, eigene Extension Functions zum Schreiben von Assertions mitzuliefern, die den Code idiomatischer aussehen lassen. Darin enthalten sind beispielsweise Varianten von assertAll() und assertThrows(), die erlauben, die erwartete Exception-Klasse als Reified Type Parameter zu übergeben bzw. Kotlin Closures direkt zu verwenden (Listing 7).

Da Kotlin nahezu beliebige Methodennamen erlaubt, kann man seinen Tests häufig besser lesbare Namen geben, ohne die @DisplayName-Annotation zu verwenden.

Des Weiteren kann man mit @TestInstance(Lifecycle.PER_CLASS) den Standardlebenszyklus von Testklassen so verändern, dass sich alle Tests eine Testinstanz teilen. Normalerweise erzeugt JUnit für jeden Test eine neue Instanz. Das zieht nach sich, dass Methoden, die mit @BeforeAll oder @AfterAll annotiert sind, normalerweise static sein müssen. Für letzteres benötigt man in Kotlin allerdings ein Companion Object, d. h. zusätzliche Syntax, die vom eigentlichen Test ablenken würde. Im Beispiel in Listing 7 ist es eigentlich unnötig, @BeforeAll zu verwenden, da der Aufwand zum Erzeugen einer Calculator-Klasse vernachlässigbar ist.

Parallele Testausführung

Eins der spannendsten neuesten Features ist mit Sicherheit die in Version 5.3 eingeführte Möglichkeit zur parallelen Testausführung. Standardmäßig werden Tests nach wie vor sequenziell ausgeführt. Mit Hilfe des junit.jupiter.execution.parallel.enabled-Configuration-Parameters, den man zum Beispiel als System Property angeben kann, lässt sich das Feature aktivieren. So geschehen führt die Jupiter Engine alle Tests in gegebenenfalls nebenläufigen Threads aus. Die Anzahl der Threads lässt sich ebenfalls über Configuration-Parameter einstellen (siehe JUnit User Guide). Ohne explizite Konfiguration verwendet die Jupiter Engine die Anzahl der Prozessorkerne als Richtwert.

Nun wäre es sicherlich unrealistisch, davon auszugehen, alle Testklassen seien robust gegenüber paralleler Ausführung. Thread Safety und der Zustand externer Ressourcen stehen dem oft im Weg. Glücklicherweise können wir mit Hilfe des Jupiter APIs beeinflussen, welche Tests nebenläufig ausgeführt werden. Die Annotation @Execution(SAME_THREAD) stellt die Ausführung einer Testklasse oder -methode auf den gleichen Thread um, der für das umschließende Element, also etwa die Testklasse, verwendet wird. Des Weiteren erlaubt die @ResourceLock-Annotation, deklarativ anzugeben, welche Ressourcen – beispielsweise eine Datei, Datenbank oder global gehaltener Zustand – ein Test lesend bzw. schreibend verwendet. Die Jupiter Engine stellt sicher, dass kein Test, der eine Ressource benötigt, gleichzeitig ausgeführt wird mit einem anderen, der dieselbe Ressource gerade potenziell verändert.

Listing 8: Parallele Tests, die sicher auf geteilte Ressourcen zugreifen

@Execution(CONCURRENT)
class SharedResourcesDemo {
  @Test
  @ResourceLock(value = "system.properties", mode = READ)
  void customPropertyIsNotSetByDefault() {
    assertNull(System.getProperty("my.prop"));
  }
 
  @Test
  @ResourceLock(value = "system.properties", mode = READ_WRITE)
  void canSetCustomPropertyToFoo() {
    System.setProperty("my.prop", "foo");
    assertEquals("foo", System.getProperty("my.prop"));
  }
 
  @Test
  @ResourceLock(value = "system.properties", mode = READ_WRITE)
  void canSetCustomPropertyToBar() {
    System.setProperty("my.prop", "bar");
    assertEquals("bar", System.getProperty("my.prop"));
  }
}

Das Beispiel in Listing 8 veranschaulicht, wie @ResourceLock funktioniert. Bei der geteilten Ressource handelt es sich hier um die System Properties von Java. Die erste Testmethode greift lediglich lesend darauf zu, kann also parallel mit anderen Tests ausgeführt werden, die ebenso nur lesenden Zugriff benötigen. Die anderen beiden Testmethoden greifen schreibend und lesend auf die System Properties zu und verwenden daher den READ_WRITE-Modus. Ohne @ResourceLock-Deklaration würden alle drei Tests nebenläufig ausgeführt werden und – aufgrund der Race Condition beim Zugriff auf die my.prop-Property – gelegentlich fehlschlagen.

JUnit als Plattform

JUnit ist das meistgenutzte Testing-Framework für Java und eine der weitverbreitetsten Open-Source-Bibliotheken für Java überhaupt. Es wird dabei nicht nur von Entwicklern verwendet, die Tests schreiben. Jede IDE und jedes Build-Tool im Java-Ökosystem unterstützt das Ausführen von JUnit-Tests. Dafür haben sie bisher nicht nur das öffentliche API verwendet, sondern teilweise auch auf interne APIs oder Reflection zurückgegriffen, um auf Daten zuzugreifen, die anders nicht zugänglich waren. Des Weiteren hängen andere Testing-Frameworks wie etwa Spock  [7] von JUnit ab, da sie auf dessen Integration in Build-Tools und IDEs aufbauen.

Die Belange von Testautoren, IDEs/Build- Tools sowie anderen Testing -Frameworks sind dabei nur bedingt kompatibel. Während erstere sich Flexibilität und neue Features wünschen, brauchen die anderen Stabilität, weil sie viele verschiedene Versionen unterstützen wollen. Daher hat sich das JUnit-Team beim Entwurf von JUnit 5 entschlossen, die verschiedenen Belange durch klar separierte APIs und Komponenten voneinander zu trennen. Für Testautoren gibt es das Jupiter API, für IDEs und Build-Tools stellt die neue JUnit Platform das Launcher API und für Testing-Frameworks das Test Engine SPI zur Verfügung.

philipp_junit_3.tif_fmt1.jpgAbb. 3: Die Architektur von JUnit 5

JUnit 5 selbst enthält zwei Test-Engine-Implementierungen: Die Jupiter- Engine sowie die Vintage Engine. Letztere dient dazu, mit JUnit 3 oder 4 geschriebene Tests auf der neuen Platform auszuführen. So kann man sowohl neue Jupiter-Tests als auch alte Tests in einem Projekt gemeinsam ausführen. Auch eine schrittweise Migration ist so möglich. Darüber hinaus steht die neue Plattform anderen Testing-Frameworks zur Verfügung (Abb. 3). Wie im Wiki auf GitHub [6] zu sehen ist, gibt es bereits einige solcher Test-Engine-Implementierungen. Im JUnit 5 Samples Repository [8] findet man ein Beispiel für Ausführung verschiedener Test-Engines in einem Build.

Die neue Platform enthielt anfangs auch Plug-ins für Gradle und Maven. Mittlerweile bieten die meisten Build-Tools aber native Unterstützung für JUnit  5: Gradle seit Version  4.6, Maven Surefire seit 2.22.0 und Ant seit 1.10.3. Über den in der Platform enthaltenen ConsoleLauncher lassen sich andere Tools problemlos anbinden.

Auch der IDE-Support für JUnit 5 ist mittlerweile sehr gut. IntelliJ IDEA unterstützt die Ausführung von Tests auf der JUnit Platform bereits seit Version 2016.2, Eclipse seit  4.7.1a, der Java Test Runner von Visual Studio Code sei  0.4.0. Wer Netbeans verwendet, muss noch auf Version 10.0 warten oder in der Zwischenzeit @RunWith(JUnitPlatform) verwenden, um Jupiter-Tests mit JUnit 4 auszuführen.

Filtern mit Tags

Ein Platform-Feature, das somit allen Test-Engines offensteht, ist das Filtern der auszuführenden Tests über Tag-Expressions. Eine Tag-Expressions ist ein Boolescher Ausdruck, der die Operatoren !, & und | sowie Klammern enthalten darf. Im einfachsten Fall ist eine Tag-Expression einfach nur der Name eines Tags. Einen solchen Ausdruck kann man verwenden, um gewisse Tests zu akzeptieren oder sie herauszufiltern.

philipp_junit_4.tif_fmt1.jpgAbb. 4: Tag-Expressions in IntelliJ IDEA

Im Beispiel aus Listing 1 können wir mit der Tag-Expression addition den Test onePlusOneIsTwo() miteinbeziehen bzw. ausschließen. Die Tag-Expressions addition & multiplication selektiert alle Tests, die sowohl mit addition als auch mit multiplication gekennzeichnet sind. In unserem Beispiel ist das lediglich die divideResultOfMultiplication()-Testmethode. Das funktioniert sowohl in IntelliJ IDEA (Abb. 4) und Eclipse als auch in Build-Tools wie Maven und Gradle. Wie Listing 9 demonstriert, kann man damit etwa in Gradle eigene Tasks definieren, die nur eine bestimmte Art von Tests ausführen, zum Beispiel Integrationstests.

Listing 9: Eigener Task zum Ausführen von Integrationstests in Gradles Kotlin DSL

tasks.register<Test>("integrationTest") {
  useJUnitPlatform {
    includeTags("integration")
  }
}

Fazit

An JUnit 5 wurde vor der Veröffentlichung von Version 5.0 fast zwei Jahre entwickelt. Der aktuelle Stand erfüllt die meisten Anforderungen, die wir heutzutage an ein modernes Testing-Framework haben. Die neue JUnit Platform bietet sehr gute Integration in IDEs und Build-Tools und erlaubt uns gleichzeitig sowohl JUnit 3/4, Jupiter als auch andere Testing- Frameworks zu verwenden. Zum Einstieg gibt es neben dem User Guide eine ganze Reihe von Beispielprojekten [8] für alle gängigen Build-Tools.

Das JUnit-Team arbeitet kontinuierlich daran, weitere Verbesserungen und neue Features zu veröffentlichen. So wird etwa JUnit 5.4 die Beeinflussung der Ausführungsreihenfolge von Tests ermöglichen, zum Beispiel um diese zu randomisieren. Ein neues Reporting-Format, das die neuen Platform-Features besser unterstützt, deklarative Testsuiten, parametrisierte Testklassen und Szenariotests stehen außerdem auf der Agenda. Als Teil des JUnit-Teams kann ich versichern, dass wir uns stets über Feedback, Anregungen und sogar Bug Reports auf GitHub [9] freuen. Happy Testing!

philipp_marc_sw.tif_fmt1.jpgMarc Philipp hat mehr als zehn Jahre Erfahrung in der Entwicklung von Software für Unternehmen und Endkunden sowie als Trainer und Coach für andere Entwickler. In seiner Arbeit bei Gradle widmet er sich der Verbesserung des Entwicklungsalltags von Millionen von Entwicklerinnen und Entwicklern und kann seiner Leidenschaft für Open-Source-Software nachgehen. Er ist seit langem aktiver Committer und Maintainer von JUnit. Außerdem war er Mitinitiator der Crowdfunding-Kampagne JUnit Lambda, die der Anfang von dem war, was nun JUnit 5 ist.

Seitdem 2008 das Bitcoin Whitepaper unter dem Pseudonym Satoshi Nakamoto veröffentlicht und Anfang 2009 die ersten Bitcoins geschöpft wurden, haben sowohl Kryptowährungen als auch die genutzte Basistechnologie Blockchain weltweit ein großes Echo erfahren.

Basierend auf den Blockchainanwendungen unterteilen Beobachter die Evolution häufig in drei Phasen: Kryptowährungen wie Bitcoin stellen die Blockchain 1.0 da. Es folgt Blockchain 2.0 mit Smart Contracts (dazu mehr im Abschnitt zum Finanzsektor). Später wurden Smart Contracts in der Blockchain 3.0 zu dezentralen, autonomen Organisationseinheiten weiterentwickelt.

Im Umfeld der Blockchaintechnologie boomen zurzeit die Anwendungsfelder und Umsetzungsmöglichkeiten, die weit über eine virtuelle Währung hinausreichen. Die Technologie wird gehypt. Zu Recht? Schauen wir uns das genauer an:

Eine Konsensbildung (dezentral, weltweit verteilt) kann in bestimmten Geschäftsprozessen die Rolle eines vertrauenswürdigen Dritten innehaben – speziell bei Prozessierung und Authentisierung [1]. Dies gilt sowohl für Intermediäre im wirtschaftlichen Kontext als auch, soweit rechtlich zulässig, für Aufsichtsfunktionen bei hoheitlichen Aufgaben.

Waschechte Disruption mit Revolutionscharakter

Geschäftsmodelle vieler Organisationen und Institutionen werden durch die Blockchain infrage gestellt. Zugleich ergeben sich neue Geschäftsmodelle, die ohne diese Technologie vermeintlich nicht wirtschaftlich abbildbar wären. Das Vertrauen in einen Dritten wird abgelöst durch das Vertrauen in ein Kollektiv, in eine Technologie/Code und in die Kryptografie.

In der Blockchain können Werte abgebildet werden, deren Zugriffsrechte eindeutig und dauerhaft von einem Nutzer an einen anderen transferiert werden können. Deshalb gilt die Blockchain als Grundlage des Internets der Werte (Internet of Value) und als Ergänzung des bisherigen Internets der Informationen (Internet of Information).

Kryptowährungen sind dabei nur eine naheliegende Anwendung. Nutzer können auch Rechte an realen Werten binär in der Blockchain archivieren und damit handeln. Damit erweitert die Blockchain das Internet als Plattform des Kopierens und Teilens um eine Plattform, auf der Herkunft und Besitz von Werten unveränderlich protokolliert und deren Aktionen transparent nachvollziehbar sind.

Von Rechts wegen handelt es sich bei Smart Contracts nicht um Verträge. Das Konzept ermöglicht es durch Regeln und Ausführungsanweisungen jedoch, vorgegebene Prozesse auf der Blockchain automatisiert und dezentral auszubilden. Das erschließt ein gigantisches Automatisierungspotenzial. Dieses neue Anwendungsspektrum erstreckt sich von der Logistik über den Handel bis hin zum Internet der Dinge, kurz IoT, mit dessen Hilfe smarte Gegenstände wie beispielsweise Stromzähler ihre Nutzung selbstständig verhandeln und abrechnen können.

Die Demokratisierung der Prozesse

Grundsätzlich sind die in einer Blockchain repräsentierten Transaktionen für alle Teilnehmer im Netz sichtbar und nachvollziehbar. Zudem garantiert die Blockchain Irreversibilität, das heißt, Transaktionen in der Blockchain können nicht nachträglich einzeln manipuliert oder gelöscht werden. Um eine Transaktion rückgängig zu machen, kann lediglich – wieder im öffentlichen Konsens – die entsprechende Gegentransaktion in der Blockchain hinterlegt werden. Im Prinzip gestalten sich somit Herkunftsnachweise und Transaktionen für abgebildete Werte nahezu automatisch revisionssicher – vorbehaltlich, dass nicht alle im selben Moment die Dateien löschen.

Das eröffnet noch nicht absehbare Möglichkeiten im Bereich der Compliance bis hin zur automatisierten Prüfung bisher manuell durchgeführter Prozesse. Die klassischen Geschäftsmodelle von Wirtschaftsprüfern drohen auszusterben.

Ist keine vollständige Transparenz erwünscht, existiert die Möglichkeit, private Blockchains einzurichten, zu denen nur ein eingeschränkter Nutzerkreis Zugang hat. Außerdem gibt es inzwischen Wege, auch in öffentlichen Blockchains, beispielsweise im Darknet, die Nachvollziehbarkeit einzuschränken – mit allen Vor- und Nachteilen.

Diese Fähigkeiten brachten in den letzten Jahren viele Projekte und eine unüberschaubare Anzahl an Akteuren zutage. Das Spektrum reicht von Start-ups über Technologieunternehmen bis hin zu neu gebildeten Konsortien wie beispielsweise das Hyperledger Project. Aber auch Forschungsorganisationen, Universitäten, Individuen, Regierungen, nichtstaatliche Organisationen und Wagniskapitalgeber forschen an der nächsten „Killer-App“, die für die Blockchain das wird, was der Browser für das Internet wurde.

Je mehr Hype, desto mehr Vorsicht

Der weltweite Hype kann nicht darüber hinweg täuschen, dass es mehr Visionen, Theorien und Konzepte als real existierende, funktionierende Anwendungen gibt. Denn die junge und zugleich hochkomplexe Technik bringt facettenreiche Herausforderungen mit sich. Es fehlt an Infrastrukturen für den jeweiligen Einsatz, an adäquaten Kapazitäten und der Skalierbarkeit. Außerdem sind die Reaktionszeiten noch zu lang, das Governance-Modell sowie der entsprechende Rechtsrahmen noch nicht stimmig.

Vor diesem Hintergrund ist die kritisch-analytische Bewertung der Blockchain-Technologie eine Kernherausforderung der Wissenschaft und viele Fragen bleiben noch offen:

  • Welche Chancen und Risiken sind mit der Technologie verbunden (und für wen)?

  • Was sind Hindernisse und Treiber der Umsetzung?

  • Welche Effekte hat die Technologie zukünftig auf Wirtschaft und öffentliche Verwaltung?

  • Wie können sich Unternehmen und Behörden bei aller Ungewissheit heute am besten vorbereiten?

  • Wie lauten die technischen Forschungsfragen?

  • Welche Branchen erwarten die größten Veränderungen?

An dieser brenzligen Schnittstelle zwischen Technologie und Anwendung suchen zurzeit die Wissenschaft und noch mehr die Wirtschaft nach Orientierung. Es geht darum, frühzeitig Know-how aufzubauen, um die Blockchain-Technologie im eigenen Umfeld besser einzuschätzen und fundierte Entscheidungen über zukünftige Investitionen zu treffen – aber auch, um politische Rahmenbedingungen zu setzen.

Blockchain – der König ist tot, es lebe die Basis

Die Eigenschaft der Blockchain, Transaktionen irreversibel zu speichern und die diktatorische Hoheit einer zertifizierenden Autorität auf eine quasi basisdemokratisch verteilte Konsensfindung umzuverteilen, mutet revolutionär an. Die einen äußern unverhohlen (Schaden-)Freude, die anderen zittern um ihre Macht.

Doch was genau passiert da eigentlich? Zunächst wird die Transaktion, wie zum Beispiel die Überweisung einer Kryptowährung oder die Registrierung eines Dokuments, von einem Sender erzeugt und digital signiert. Diese Transaktion wird an das Netz gesendet und an die beteiligten Knoten verteilt. Die Knoten des Netzes überprüfen die Gültigkeit der Transaktion und fügen diese in die Blockchain ein. Bei diesem Prozess werden die Transaktionen in Blöcken gespeichert, die durch Hashfunktionen in ein standardisiertes Format überführt werden.

Die Kodierung der Aussagen ist manipulationssicher, da die Änderung bereits einer einzigen Aussage den Hashwert des Blocks verändern würde und der Hashbaum somit nicht mehr konsistent wäre. So wie ein Mikadostab, den man entfernt oder austauscht und damit das gesamte Konstrukt zusammenbrechen lässt. Blöcke werden durch Verkettung mit der bereits bestehenden Historie der Blöcke verbunden, sodass eine Kette (Blockchain) entsteht. Um einen neuen Block in die bestehende Verkettung aufzunehmen, ist bei Bitcoin ein kryptografisches Rätsel zu lösen.

Welche Zeichenkette liefert einen ähnlichen Hashwert wie die Kodierung des neu aufzunehmenden Blocks? Die Ähnlichkeit beider Werte ist durch die Anzahl der übereinstimmenden Stellen im Hashwert definiert und der Schwierigkeitsgrad der Ähnlichkeit somit variierbar. Da die Hashfunktion nicht umkehrbar ist, existiert derzeit (noch) kein konstruktives Verfahren für die Ableitung der zu erratenden Zeichenfolge für den gegebenen Hashwert. Es ist somit eine Unzahl von Zeichenfolgen auszuprobieren, was gigantische Rechenkapazitäten erfordert.

Wenn ein Knoten, das heißt ein Teilnehmer des Blockchainverbundes, eine entsprechende Zeichenfolge gefunden hat (Mining), wird der neue Block als Element in die Kette aufgenommen (Blockchain) und damit zum letzten gültigen Block in der gesamten Kette. Für jeden anderen Knoten im Netz ist die Korrektheit einfach nachzuvollziehen, indem lediglich ein Hashwert zu berechnen ist. Somit lässt sich eine korrekte Verkettung von Blöcken zu einer Blockchain realisieren.

Für die Persistenz werden diese Ketten nun über eine Vielzahl von Knoten verteilt, das heißt, alle Knoten haben dasselbe Basiswissen. Entstehen in einzelnen Knoten nun neue Blöcke als Ergänzung der bestehenden Blockchain, so ist im gesamten Netz ein Konsens über die Änderung zu erzielen. Für diese Konsensfindung dient das kryptografische Rätsel.

Die Mehrheit fungiert als Türsteher

Sobald ein Knoten ein Rätsel gelöst hat, wird die Lösung von allen geprüft und übernommen. Blöcke, die noch auf Konsensfindung warten, sind in einer Nachrückerliste organisiert, in der ebenfalls Blöcke parallel entstandener Verkettungen aufgenommen werden, um sie wieder in die eine globale Blockchain zu integrieren. Nur wer Konsens findet, darf rein.

Eine Blockchain mit ihren einzelnen Blöcken lässt sich so in einem Netz von Knoten verwalten. Über die Konsensfindung wird festgelegt, welcher Block als nächstes Element in die globale Blockchain übernommen wird. Ursprünglich wurde das kryptografische Rätsel für die Erzeugung neuer Blöcke (Mining) genutzt, was als Proof of Work bezeichnet wird. Für unterschiedliche Vertraulichkeits- und Sicherheitsanforderungen kann die Schwierigkeit des Rätsels übrigens angepasst werden.

Ein Dokumentationssystem beispielsweise für die Verteilung von Stromverbräuchen in einem Smart Grid kann mit einfachen Rätseln arbeiten und somit auch die Rechenleistung der Steuerungsknoten berücksichtigen. Weitere Arten der Konsensfindung können beispielsweise Anteilsscheine an einem System berücksichtigen. Ein Konsens ist erzielt, wenn die Mehrheit der Anteilsinhaber (Proof of Stake) zum gleichen Ergebnis kommt.

Alternativ können Knoten als Miner für die Konsensfindung ausgezeichnet (Umpires) werden oder es kann eine lotterieorientierte Auswahl erfolgen. Darüber hinaus gibt es weitere Möglichkeiten und Kombinationen der Konsensfindung. Blockchains lassen sich so vereinfacht als verteilte Datenbanken beschreiben, die durch die Teilnehmer im Netz organisiert werden.

Was macht die Blockchain so sexy – und zugleich so hungrig?

Gegenüber klassischen, zentralen Ansätzen sind Blockchains weniger fehleranfällig und beugen insbesondere byzantinischen Fehlern vor. Allerdings bringen diese Systeme auch verschiedene Herausforderungen mit sich. Besonders kritisch wird derzeit die hohe Redundanz der Daten diskutiert. Durch vielfaches Vorhalten der gleichen Daten im Netz wird sehr viel Speicherplatz benötigt. Weiterhin beschränken die Konsensmechanismen häufig die Leistungsfähigkeit der Blockchain.

Obwohl die Blockchaintechnik noch am Anfang der Entwicklung steht, hat sie in der jüngeren Vergangenheit diverse Veränderungen erfahren, die vor allem die Nutzung in einem geschlossenen Unternehmenskontext betreffen. Aufgrund der unterschiedlichen Zielsetzungen besteht ein grundsätzlicher Unterschied zwischen öffentlichen (Public) und nichtöffentlichen (Private) Blockchains.

Public Blockchains sind öffentliche Systeme, auf die jeder, der eine Kopie besitzt, zugreifen kann. Das ist nicht gleichbedeutend mit dem automatischen Lesen und Schreiben auf einer Blockchain. Dies erfolgt über sogenannte Full Nodes, die die genehmigungsfreien Anfragen eines Anwenders bearbeiten. Beispiele für öffentliche Systeme sind Ethereum oder die First Generation Blockchain hinter Bitcoins.

Private Blockchains beschreiben Systeme, die nur für ein abgeschlossenes Konsortium, zum Beispiel von Organisationen, verfügbar sind. Der Öffentlichkeitscharakter ist von der Frage nach den Zugriffsrechten zu unterscheiden. Public Blockchains sind häufig genehmigungsfrei (permissionless). Bei Private Blockchains werden Zugriffsrechte in der Regel administriert beziehungsweise auf ein Konsortium beschränkt (Consortia Blockchain). In den meisten Fällen handelt es sich um genehmigungsbasierte Blockchainsysteme. Das populärste Beispiel für eine Private Blockchain ist Hyperledger.

xCurrent hingegen wird fälschlicherweise als Blockchain bezeichnet, ist es jedoch nicht. Es handelt sich um einen bidirektionalen Nachrichtenaustausch [4].

EU-DSGVO

Wir rufen uns in Erinnerung, was personenbezogene Daten laut EU-DSGVO Artikel 4, Begriffsbestimmung sind:

„Personenbezogene Daten“ bezeichnen alle Informationen, die sich auf eine identifizierte oder identifizierbare natürliche Person (im Folgenden „betroffene Person“) beziehen. Als identifizierbar wird eine natürliche Person angesehen, die direkt oder indirekt, insbesondere mittels Zuordnung zu einer Kennung wie einem Namen, zu einer Kennnummer, zu Standortdaten, zu einer Onlinekennung oder zu einem oder mehreren besonderen Merkmalen identifiziert werden kann, die Ausdruck der physischen, physiologischen, genetischen, psychischen, wirtschaftlichen, kulturellen oder sozialen Identität dieser natürlichen Person sind [5].

Da wir naturgemäß Daten in einer Blockchain nicht wirklich ändern beziehungsweise sie aus ihr löschen können, ist der einzig mögliche Weg, diese personenbezogenen Daten erst gar nicht in eine Blockchain abzuspeichern. Damit die Sinnhaftigkeit beispielsweise von Supply-Chain-Ketten erhalten bleibt, sieht mein Ansatz wie folgt aus:

Wir generieren für sinnhafte Einheiten eindeutige Identifier mit einer Applikation und schreiben diese in eine einfache, von der Blockchain getrennt gehaltene Datenbanktabelle. Also für den Namen ID_A, für die Adresse ID_B etc. Wichtig hierbei ist, dass dies jedes Mal erneut geschieht. Wenn der Name Pierre Gronau nochmals auftaucht, bekommt dieser die ID ID_C. Die Applikation speichert also die Identifier in die Blockchain und nicht den Namen.

Wenn nun die Information abgeändert oder gelöscht werden muss, erfolgt das in der separaten SQL-Datenbank. Datenschutzanforderungen werden nun umsetzbar. Um die Sache zu steigern, fügen wir hier noch die Compliance-Anforderungen für Datenhaltung hinzu:

Datenschutzkonforme Systementwicklungen fordern, Daten redundant zu speichern und sie mindestens in die Kategorien Informationsdaten, widerrufliche Einwilligungsdaten und Archivierungsdaten einzuteilen [5]. Mehr dazu im zweiten Artikel dieser Serie.

Ein mögliches Szenario: Identitäts- und Access-Management

Laut Zookos Dreieck [6] kann ein Namensraum in einem Computernetz nur zwei von drei der folgenden Eigenschaften gleichzeitig erfüllen:

  • sicher – Authentizität muss gewährleistet werden (möglich mit Kryptografie),

  • dezentralisiert – es gibt keine vertrauenswürdige zentrale Instanz, die die Namen verwaltet,

  • aussagekräftig – für Menschen lesbare Namen, die von Menschen ausgewählt werden können, und nicht automatisch generierte, zufällige Zeichenfolgen.

Für diese Alternative gilt es jedoch noch den Nachweis zu erbringen, ob dies vor allem im Kontext mit der DSGVO nachhaltige Vorteile gegenüber bisherigen Techniken bringt. Ich habe da momentan meine Zweifel.

Fazit

Mein abschließender Rat zu diesem Thema lautet: Binden Sie bei Ihren Projekten rechtzeitig den Datenschutz ein. Wer die DSGVO zu Ende denkt, darf DSGVO-relevante Daten nicht in die Blockchain speichern, weil der Anwendungsfall dann sinnbefreit ist. Ich dokumentiere das durch ein anschauliches Beispiel einer Krankenkasse, die Gesundheitsdaten in der Blockchain speichern will und dabei nur noch IDs nutzt. Es bleibt dann fast zwangsläufig nur: Verzicht auf Blockchains.

gronau_saas_3_1.tif_fmt1.jpg

Pierre schuldet Alice Geld für das Mittagessen. Er installiert eine App auf seinem Smartphone, um eine neue Bitcoin Wallet zu erstellen. Eine Wallet-App ist wie eine mobile Banking-App und eine Wallet ist wie ein Bankkonto.

gronau_saas_3_2.tif_fmt1.jpg

Um sie zu bezahlen, benötigt er zwei Informationen: seinen privaten und ihren öffentlichen Schlüssel.

gronau_saas_3_3.tif_fmt1.jpg

Pierre bekommt Alices öffentlichen Schlüssel, indem er einen QR-Code von ihrem Handy abfotografiert oder indem sie ihm die Zahlungsadresse, eine Reihe von scheinbar zufälligen Zahlen und Buchstaben, mailt. Jeder, der einen öffentlichen Schlüssel hat, kann Geld an eine Bitcoin-Adresse senden, aber nur eine vom privaten Schlüssel generierte Signatur kann Geld davon freigeben.

gronau_saas_3_4.tif_fmt1.jpg

Die App warnt Bitcoin Miner in der ganzen Welt vor der bevorstehenden Transaktion. Miner bieten Transaktionsüberprüfungsdienste an.

gronau_saas_3_5.tif_fmt1.jpg

Die Miner überprüfen, ob Pierre genug Bitcoins hat, um die Zahlung sicherzustellen.

gronau_saas_3_6.tif_fmt1.jpg

Viele Transaktionen laufen im Netz zu jeder Zeit ab. Alle ausstehenden Transaktionen in einem bestimmten Zeitraum werden zur Überprüfung gruppiert (in einem Block). Jeder Block hat eine eindeutige Identifizierungsnummer, Erstellungszeit und Referenz auf den vorherigen Block.

gronau_saas_3_7.tif_fmt1.jpg

Der neue Block wird ins Netz gestellt, sodass Miner überprüfen können, ob ihre Transaktionen legitim sind. Die Verifizierung erfolgt durch die Durchführung komplexer und sicherer kryptografischer Berechnungen.

gronau_saas_3_8.tif_fmt1.jpg

Wenn ein Miner das kryptografische Problem gelöst hat, wird dem Rest des Verbunds die Entdeckung mitgeteilt.

gronau_saas_3_9.tif_fmt1.jpg

Der Algorithmus belohnt den gewinnenden Miner mit 25 Bitcoins und der neue Block wird an die Vorderseite der Blockkette hinzugefügt. Jeder Block verbindet den vorherigen Block, sodass eine Kette entsteht: die Blockchain.

gronau_saas_3_10.tif_fmt1.jpg

Innerhalb von zehn Minuten nachdem Pierre die Transaktion eingeleitet hat, erhalten er und Alice jeweils die erste Bestätigung, dass die Bitcoins an sie weitergeleitet wurden.

gronau_saas_3_11.tif_fmt1.jpg

Alle Transaktionen im Block sind nun erfüllt und Alice erhält das Geld

gronau_pierre_sw.tif_fmt1.jpgPierre Gronau ist Gründer der Gronau IT Cloud Computing GmbH. Seit zwanzig Jahren arbeitet er als Senior-IT-Berater mit umfangreicher Projekterfahrung. Zu seinen Kompetenzfeldern gehören Servervirtualisierungen, Cloud Computing und Automationslösungen sowie Datensicherheit und IT-Datenschutz. Seine Analyse- und Lösungskompetenz dient Branchen von Gesundheitswesen bis Automotive, von Telekommunikation bis Finanzwesen als Orientierung für Unternehmensentwicklung in puncto Digitalisierungsstrategien und IT-Sicherheitskonzepte.