Teststrategie

Mithilfe der Schleupen.CS-Testpyramide wird definiert, wie fachliche Funktionalität sichergestellt wird. Dies wird durch eine möglichst hohe Testabdeckung erreicht, wobei alle Bausteine, aber auch Kopplungspunkte getestet werden. Eine automatisierte Durchführung ist zwingend erforderlich, um eine geeignete Qualität dauerhaft sicherstellen zu können. Hierzu verwendet Schleupen eine Continuous-Delivery-Pipeline, die bei jedem Check-In angestoßen wird und die verschiedenen Tests einer Teststufe in verschiedenen Schritten ausführt.

Im Folgenden wird hierdurch eine Teststrategie definiert, die auf einer Testpyramide fußt und der Orientierung dient. Zudem erreichen wir hiermit eine klarere Fokussierung bei der Erstellung von Tests.

Testpyramide

Warum Pyramide?

Die Testpyramide besteht aus mehreren Teststufen, die gleichartige Tests beinhalten. Je höher eine Teststufe in der Pyramide angesiedelt ist, desto teurer die Implementierung im Allgemeinen. Denn die Entwicklung zahlreicher Geschäftsprozesstests (als Beispiel für höherwertige Tests) sind in der Entwicklung deutlich aufwendiger als die Erstellung von Unittests. Zudem ist eine vollständige Codeabdeckung durch Geschäftsprozesstests unmöglich. Hinzu kommen längere Durchlaufzeiten von höherwertigen Tests, so dass diese eine zeitnahe Auslieferung konterkarieren. Dennoch sind diese höherwertigen Tests bindend notwendig, um das Zusammenspiel der Bausteine sicherzustellen. Um ein geeignetes Maß zu finden, wird die Anzahl der unterschiedlichen Testarten durch eine Pyramide ausgedrückt: Tests der obersten Ebene sind weniger zu erstellen, Tests der untersten Ebene gibt es am meisten. Unittests werden auf der untersten Ebene der Testpyramide erstellt. Diese benötigen eine sehr hohe Codeabdeckung, um Codeänderungen nahezu gefahrlos durchführen zu können. Die Durchlaufzeiten von Unittests sind gering, Unittests sind einfach zu implementieren und zu warten.

Damit ergibt sich in Summe das Ziel, eine möglichst flache und breite Pyramide zu erstellen. Denn je früher ein Fehler gefunden wird, desto geringer der Schaden und desto billiger die Reparatur.

Die Testpyramide ist wie bereits erwähnt in Teststufen unterteilt. Die Definition der Teststufen ergibt sich also daraus, dass eine Teststufe für Tests definiert wird, die sich signifikant in der Anzahl unterscheiden. Insbesondere ist hierbei die Ausführungszeit und der Implementierungsaufwand, aber auch das Testziel relevant. Dabei sollte ein Test nur genau ein Testziel haben (und damit aussagekräftig sein).

Schleupen-Testpyramide samt Teststufen

In Schleupen.CS definieren wir folgende Teststufen, die z.T. noch untergliedert sind:

  • QS – Betriebliche Tests
    Betriebliche Tests sind Massentests, Performance-Tests, Installationstests usw., aber auch explorative Tests durch Menschen.
  • GPT – Geschäftsprozess-Tests
    Geschäftsprozesstests testen Geschäftsprozesse (oder auch Marktrollen-übergreifende Verbundprozesse) von Ende zu Ende und weisen somit ein hohe Komplexität auf. Auch die Durchlaufzeiten sind deutlich höher als bei Tests niedrigerer Teststufen.
  • AIT – (Automated) Integrative Tests
    Diese Tests beinhalten Tests, die Abschnitte von Geschäftsprozessen testen. Das sind beispielsweise Dialogabläufe oder Workflows. Aber auch integrative Tests innerhalb von Ländern und Geschäftsprozesskomponenten werden hierzu gezählt.
  • CD – Tests im CD-Build
    Tests im CD-Build haben eine sehr kurze Durchlaufzeit und sind Unittests und Tests im Zusammenspiel mit einer lokalen Datenbank als einzige Abhängigkeit. 

Strategie - Wann schreibt man Tests für welche Stufe?

Generell stellt sich die Frage, wie man entscheidet, ob Tests auf einer bestimmten Teststufe erstellt werden sollten. Hier verwenden wir bei Schleupen folgende Strategie: Wird ein Fehler auf einer höheren Stufe entdeckt (beispielsweise im Regressionstest), so sollte zunächst ein Unittest erstellt werden, der den Fehler nachstellt. Schleupen.CS ist in der Mikroarchitektur derart konzipiert, dass für alle Klassen Unittests erstellt werden können. Damit kann also einfach eine breite, flache Testpyramide erreicht werden. Sollte dies nicht ausreichend sein - z.B. bei Benutzeroberflächen - so wird ein Test auf der nächsthöheren Stufe erstellt etc.

Welche Tests sollten auf höheren Ebenen definiert werden? Hier gibt es kein festes Kriterium, wohl aber eine Daumenregel:
Je wichtiger der Testfall für das Geschäft des Kunden, desto eher ist dieser als höherwertiger Test (z.B. Geschäftsprozess-Test) zu implementieren, um die Funktionalität in Summe sicherzustellen.

Ausführungsstufe in Pipeline

Da bei Schleupen eine Continuous-Delivery-Pipeline verwendet wird, werden diese Stufen entsprechend wie folgt abgebildet:

Continuous-Delivery-Pipeline in Schleupen.CS

Dabei werden durch ein Check-In die Tests der ersten Stufe ausgeführt. Sind für den Baustein alle Tests der ersten Stufe - also die Unittests, ComponentTests und Repository.ConnectionTests - erfolgreich durchlaufen, so hat er die erste Stufe der Continuous-Integration bestanden. Danach durchläuft der Baustein im Erfolgsfall die weiteren Stufen Development, Release-Candidate und Certified-Release, wobei jeweils die zugehörigen automatisierten Tests ausgeführt werden.

Ziel ist es, dass letzten Endes die Pipeline entscheidet, ob die durch die Pipeline geschickte Auslieferungseinheit ausgeliefert werden kann oder nicht.

Teststufen

Im Folgenden werden die einzelnen Teststufen genauer betrachtet. Wir gehen dabei von unten nach oben vor.

Wichtig zu beachten ist, dass Tests nur genau ein Testziel haben sollten. Bei einer Mischung von Performance-Tests und fachlichen Tests wäre dies beispielsweise nicht gegeben. Hierdurch verbessert sich dann auch die Lesbarkeit und das Verständnis des jeweiligen Tests.

Unittests

Unittests testen im Standard nur eine einzelne Klasse. Nur im Domänenmodell darf hiervon leicht abgewichen werden und sich diese auch auf ein Aggregat beziehen. Denn ein Aggregat hat eine Konsistenzgrenze die sichergestellt werden muss und in Schleupen.CS hierfür keine eigene Teststufe ausgeprägt worden ist. Wichtig ist in jedem Fall, dass keine Infrastruktur verwendet wird. 

Mithilfe von Unittests können Whitebox-Tests und Blackbox-Tests implementiert werden. Die Ausführungszeit ist per se gering, da keinerlei Infrastruktur verwendet werden darf. Auch ist die Implementierung hierdurch einfach.

Die folgende Abbildung zeigt, an welchen Stellen, welche Art von Unittests in Ländern empfehlenswert ist. Prinzipiell können alle Bausteine per Unittest getestet werden.

Für das Codieren von Unittests kommen geeignete Hilfsmittel wie Testdouble (Mocks, Stubs, Testspy, etc.) beispielsweise bei Usecase-Controllern zum Einsatz. Hierzu verwenden wir das Framework Moq.

Unsere Klassen sind im Standard per Strategiemuster verknüpft, sodass die Schnittstelle i.A. durch ein Test-Double ersetzt werden kann:

Zur vereinfachten Testdatenerstellung wird u.a. das Framework AutoFixture genutzt.

Unittests in Ländern

Die folgende Abbildung zeigt, wo Unittests in GP-Komponenten bei Workflows zum Einsatz kommen.

Unittests für Workflows in GP-Komponenten
Unittests für UI-Elemente in GP-Komponenten

Analog können Unittests in GP-Komponenten für UI-Elemente erstellt werden.

In einer typischen Solution werden die Projekte bzgl. der Teststufen getrennt. So befinden sich die Unittests in Projekten mit dem Postfix UnitTests, wie in der folgenden Abbildung für eine Land-Solution exemplarisch dargestellt, ersichtlich ist. Damit kann die Zuordnung zu den Schritten der Continuous-Delivery-Pipeline einfach zugeordnet werden und man behält besser Übersicht.

Test-Projekte in einer Land-Solution gemäß Konvention

Unittests werden durch Entwickler implementiert.

Spezielle Klassen des Schleupen.CS-Frameworks vereinfachen die Arbeit bei der Erstellung von Unittests - so kann beispielsweise ein bereitgestellter Test verwendet werden, der den Zusammenbau der Serviceimplementierung per Dependency-Injection-Container sicherstellt. Diese sind in engerem Sinne kein Unittests.

Zur Erstellung von Unittests eignet es sich, diese gemäß des Paradigmas Test-getriebener Entwicklung (TDD) zu implementieren. Hierbei sind wir allerdings nicht dogmatisch - am Ende ist das Resultat entscheidend, nämlich das sinnvolle Tests gemäß Äquivalenzklassenbildung, Grenzwertverfahren, etc. erstellt werden und ein gute Software-Design entstanden ist.

ComponentTests

ComponentTests testen einen Baustein (= Komponente) isoliert, d.h. ohne Infrastruktur und ohne andere Bausteine (Länder oder GP-Komponenten). Hierbei werden Tests von sinnvollen Szenarien / Geschäftsvorfällen (d.h. nicht Prozesse!), mit echter Testdatenbank für das getestete Land ohne weitere externe Ressourcen, also keine echten externen Services, durchgeführt. Ziel dieser Tests ist das Sicherstellen der Funktionalität der Komponente primär als BlackBox-Tests. Es wird das Zusammenspiel der einzelnen Bausteine sichergestellt.

ComponentTests haben eine relativ geringe Ausführungszeit und können entsprechend zur Continuous-Integration verwendet werden. Sie sind allerdings langsamer als Unittests, da eine Datenbank als einzige externe Komponente erlaubt ist.

An den "Nahtstellen" der Komponente wie zum Beispiel Gateways werden Mocks oder andere Test-Double verwendet, um ein geeignetes externes Verhalten zu emulieren.

Die folgenden Abbildungen zeigen dies auch noch einmal für Workflow-Tests und UI-Tests in GP-Komponenten.

Mocking externer Komponenten bei ComponentTests in Workflows
Mocking externer Komponenten bei ComponentTests in UI-Elementen

Analog zu den Projekten für Unittests, werden Projekte zur klaren Trennung der Tests für ComponentTests definiert. Exemplarisch kann man dies am Beispiel eines Landes sehen:

Projekte für ComponentTests in einer Solution

ComponentTests werden durch Entwickler implementiert.

ConnectionTests

Die Teststufe der ConnectionTests dient zum Testen des Anschlusses externer Ressourcen. Dies ist z.B. die Datenbankanbindung, Nutzung externer Services oder das Dateisystem. Diese Tests gehören zu den Automated-Integrative-Tests (vgl. Pipeline).

ConnectionTests in Ländern

Diese Teststufe teilt sich in zwei Stufen auf, die auf verschiedenen Ebenen der Pyramide angesiedelt sind und in der Continuous-Delivery-Pipeline zu unterschiedlichen Zeitpunkten ausgeführt werden.

Repository.ConnectionTests

Diese Tests werden im CD-Build ausgeführt und testen insbesondere die Implementierung von Repositorys im Zusammenspiel mit einer Datenbank. Aus der Erfahrung heraus hat sich hier eine echte Datenbank als vorteilhaft herausgestellt, wobei die Laufzeit hier natürlich höher ist als bei Unittests.

Die Projekte sind hier analog organisiert:

Projekte für ConnectionTests in einer Solution

ConnectionTests werden durch Entwickler implementiert.

ContractTests (fällt als Kategorie mit ComponentTests zusammen)

Mithilfe von Consumer-Driven Contract-Tests werden Service-Schnittstellen bezüglich der Schemata & Semantik anhand „echter“ Einsatzszenarien aus Nutzungssicht überprüft (Regressionstest, Inhalt i.d.R. von nutzenden Teams). Dabei wird zum einen durch den Bereitsteller des Service durch Tests sichergestellt, dass das Ergebnis eines Service-Calls dasselbe ist wie das vom Client erwartete. Diese Erwartungshaltung wird in Form eines Contracts definiert, der sowohl syntaktische als auch semantisches Kompatibilität sicherstellt. Gleichzeitig kann im Service diese Erwartungshaltung im Mock des Service-Client codiert werden, so dass das echte Zusammenspiel von Nutzer und Service nicht mehr integrativ getestet werden muss. Es reichen höherwertige integrative Tests wie unten beschrieben.

Auf integrative Tests kann nicht vollständig verzichtet werden, da mit diesem Konzept derzeit nur synchrone Aufrufe, nicht aber asynchrone Verarbeitungen getestet werden können (Jobs, Messaging, usw.).

ContractTests werden ähnlich wie ComponentTests implementiert, so dass nur der Unterschied durch den Ersteller existiert. Wenn möglich, werden diese Tests dem betreuenden Team des Microservice übergeben, um hierdurch möglichst früh Rückmeldung zu bekommen. Bei externen Teams geht dies natürlich nicht.

Diese Tests haben noch eine relativ geringe Ausführungszeit zu Geschäftsprozesstests. Die schematische Nutzung von Land-Services zeigt folgende Abbildung:

Äquivalent ist dies für Services von GP-Komponenten.

Implementiert werden diese Tests durch Entwickler als Blackbox-Tests.

Das Vorgehen zur Erstellung dieser Tests ist in Muster: ContractTest für WCF-Services beschrieben.

Integrative Tests

Integrative Tests einer Komponente mit weiteren Ressourcen wie insbesondere Services werden ebenfalls auf AIT-Ebene in der Pipeline ausgeführt, da sie eine spürbar höhere Laufzeit und Kopplung aufweisen. Diese Tests einer Komponente stellen das Zusammenspiel mit anderen direkten Nachbarkomponenten sicher.

Auch diese Tests werden vornehmlich durch Entwickler implementiert. Dann werden hierzu ebenfalls eigene Test-Projekte wie hier im Beispiel eines Landes angelegt.

Projekte für integrative Tests in einer Solution

In ausgewählten Szenarien (z.B. durch Verwendung von Pester) werden hier auch Tests gemäß Behavior-Driven-Development durch Tester erstellt.

Um Autonomie bei den Teams zu verbessern, ist es häufig sinnvoll, Services insbesondere anderer Teams, die bei den integrativen Tests genutzt werden, zu mocken / zu faken.

Hierzu verwenden wir einen sogenannten FakeServer, der auf WireMock.Net basiert.

Dieser kann insbesondere sowohl REST-Services als auch SOAP-Services (insbesondere der WCF) faken und konfigurierbare Ergebnisse zurückgeben. Hierzu wird in der Service Registry eine anderer Endpunkt - der des FakeServers - eingetragen.

Die Verwendung des FakeServers ist für integrative Tests sinnvoll. Besser ist es, beispielsweise Consumer-Driven Contract Test im Sinne von Shift-Left in der Pipeline zu implementieren.

Testen: Grundlegende Nutzung des Fake-Servers


(Teil-)integrative Tests des eigenen Teams

Oftmals sind Geschäftsprozesse (und/oder Verbund-Prozesse) und deren Tests (noch) nicht vorhanden, man möchte aber sofort wissen ob die Funktionalität der eigenen Bausteine integrativ funktioniert. Hierfür ist es dann sinnvoll, Tests zu implementieren, die die eigenen Bausteinen im Zusammenspiel testen.

Auch für spezifische Tests, die Funktionalität sicherstellen, die in Geschäftsprozessen nicht auffallen würden, können integrative Tests sinnvoll sein. Das kann Beispiels das Sicherstellen der Funktionalität des Cachings, spezifische UI-Tests sein.

Teilintegrative Tests ggf. wieder entfernen, wenn

  • GP- oder Verbundprozess-Tests dasselbe Testziel haben
  • und dieselbe Funktionalität sicherstellen
  • und denselben Pfad abdecken

Die folgende Abbildung zeigt, wie derartige Tests im AIT umgesetzt werden können. Dabei testet man nur die eigenen Bausteine für eine Kontext (z.B. eine bestimmte Marktrolle). Im AIT werden (primär) nur fachliche Tests ausgeführt, wobei ein Test eine maximale Laufzeit von fünf Minuten nicht überschreiten darf. Die angestrebte Laufzeit eines AITs beträgt derzeit vier Stunden.

Für die eigenen AITs können die Bausteine unterschiedliche Qualitätsstufen erreicht haben, z.B. CI und Dev gemischt.

UI-Tests als Beispiel für (Teil-)integrative Tests

Benutzeroberflächen könnte mitunter recht komplex sein, so dass bestimmte Szenarien nicht in höheren Teststufen wie Geschäftsprozess- oder gar Verbundprozess-Tests abgebildet werden. Demnach ist es in einigen Fällen sinnvoll Tests zu implementieren, die das Klicken des Benutzer emulieren.

Hierzu verwenden wir Playwright plus Erweiterungen, die wir durch unser Framework bereitstellen.

Bausteine von GP-Abschnitt-Tests

Auch hier ist häufig die Verwendung eines FakeServers sinnvoll!

GP-Tests

Tests, die ganze End-To-End-Geschäftsprozesse testen, sind aufwendig zu implementieren und zu warten und weisen teils eine hohe Komplexität auf. Bei der Ausführung ist es wichtig, dass sie auf klar definierte Datenstände und klar definierte Systemzustände aufsetzen um möglichst deterministische Ergebnisse zu erzielen. Auch ist es hier wichtig, nicht weitere Testziele wie Lasttest o.ä. reinzumischen. Das Testziel ist die Sicherstellung der Funktionalität des Gesamtprozesses.

GP-Tests benötigen zur parallelen Ausführung isolierte Testdaten

Die Geschäftsprozesstests teuer in der Erstellung sind, sollten zuerst die Geschäftsrelevantesten umgesetzt werden.

Weitere Anforderungen an die Testdaten sind:

  • Testdaten müssen für jeden Testfall (= GP-Test) isolierbar sein, damit Tests parallel ausführbar sind und man möglichst schnell ein Gesamttestergebnis erhält. Hierbei wird zwischen Testdaten, die nicht geändert werden (Read-Only-Daten) und Daten, die durch den Test geändert werden (Write-Enabled), unterschieden. Letztere müssen individuell pro Test sein, um Kollisionen bei paralleler Testausführung zu vermeiden.
Daten werden unterschiedlich bei der Ausführung genutzt, was bei Parallelität zu beachten ist
  • Testdaten sollen möglichst nah am echten Szenario sein.
  • Testdaten müssen der EU-DSGVO genügen.
  • Testdaten müssen im Einklang mit Softwareständen versioniert werden können.
Vorgehen

Wir betrachten etwas vereinfacht die Umsetzung eines integrativen Tests eines Geschäftsprozesses, den die Teams X und Y umsetzen. Dabei sei Team X das Lead-Team, das die Umsetzung koordiniert.

Dabei benötigen die Bausteine (d.h. Workflows, Benutzeroberflächen und Services) von Team X Services von Team Y. Diese Kopplung soll vermieden werden, damit möglichst autonom entwickelt werden kann. Die Kopplung sei dünn - in diesem Fall wird die Schnittstelle S1 verwendet. Um die Bausteine von Team Y nicht zu benötigen, werden die Service-Aufrufe gefaket. In unserem Fall also S1.

In der nachfolgenden Abbildung sind alle Schritte zusammengefasst die notwendig sind, um den Fake-Server zu konfigurieren. Diese müssen nicht notwendigerweise in der dargestellten Reihenfolge durchlaufen werden.

Prozess erstellen

Zunächst muss der Prozess erstellt werden. Dazu muss die Schnittstelle möglichst genau abgestimmt werden. Insbesondere Team-übergreifend genutzte Schnittstellen wie S1 sollte möglichst schmal sein, um die Kopplung gering zu halten (d.h. möglichst gut kapseln mit so wenig Informationen wie möglich).

GP-Abschnitttest erstellen

Nun wird der Test erstellt und die Fake-Server Konfiguration für S1 definiert. Die Definition erfolgt zunächst auf Basis erwarteter Daten. Der Test ist dann lauffähig und die Implementierung der Schnittstelle S1 kann zeitlich unabhängig erfolgen.

Ideal ist es, wenn die Erwartungshaltung seitens des Service-Nutzers über Consumer Driven Contract Tests gesichert wird!

Test ausführen

Der Test sollte möglichst früh ausgeführt werden, wobei dieser durch den lokal ausgeführten automatisierten Test ("NUnit-Test") direkt ausgeführt kann. Auch in der Pipeline lässt sich derselbe Test mit anderer Konfiguration ausführen, so dass ein zentral (d.h. in der VM) gehosteter Fake-Server verwendet wird. Dies ist notwendig, um Tests parallel und somit performant ausführen zu können. Damit der Fake-Server verwendet wird, muss der Service-Endpunkt in der Service-Registry umgeschrieben werden.

Betriebliche Tests

Unter betrieblichen Tests verstehen wir Tests, die den Betrieb der Software emulieren. Hierzu gehören

  • Massen-Tests
  • Lasttests
  • Erst-Installationstests
  • Cluster-Tests
  • Explorative Tests durch Benutzer
  • Backup-/Recovery-Tests
  • Sicherheitstests
  • ...

Wichtig dabei ist, dass es sich dabei um Tests handelt, die sehr aufwendig in der Umsetzung sind und Laufzeiten aufweisen, die der Idee von Continuous Delivery widersprechen. Daher führen wir diese Tests zyklisch parallel zur Pipeline aus - immer dann, wenn ein neuer Software-Stand ausgeliefert worden ist.

Die hierbei auftretenden Fehler werden genauso bewertet wie Fehler, die durch unsere Kunden gemeldet werden.

Chatbot not allowed
Cookie Consent mit Real Cookie Banner