Das Schöne in der verteilten embedded Entwicklung ist, dass man mit vielen unterschiedlichen Entwicklungs- und Test Ansätzen in Berührung kommt und einen Erfahrungsschatz sammelt.
Mit diesem Blog zum Software Modul- und Integrationstest teilen wir diesen „Schatz“ mit euch und freuen uns über rege Diskussionen und Kommentare dazu.
1. Motivation
Unsere Motivation für diesen Beitrag
Wer kennt sie nicht: die Herausforderung „im Kontext funktionaler Sicherheit gegen nicht spezifiziertes Verhalten zu prüfen?“
Das ist unerlässlich für sicherheitskritische Projekte im embedded Umfeld.
Softwaretests können mit verschiedenen Ansätzen im Projekt etabliert werden. Wir stellen die spezifischen Vor- und Nachteile einiger dieser Ansätze durch ein fiktives und stark vereinfachtes Beispiel vor. Verwendet werden als kommerzielles Testtool “Tessy” (Razorcat) und ein selbst entwickeltes Testtool. In unserem Beispiel fließen die Anforderungen an Methoden, an den Testumfang und die Testtiefe grundsätzlich nach IEC 61508 sowie produktspezifische Sicherheitsstandards ein.
Die Gegenüberstellung des eigenentwickelten Testframework zum kommerziellen Testtool, inklusive entsprechendem Testkonzept, ist das Ziel dieses Beitrags. Wir möchten zeigen, dass sich je nach Auswahl der Toolchain, Vor- und Nachteile ergeben.
2. Ein paar Basics
Toolgestützte Tests und Test Frameworks
Intelligente Testentwicklung erfordert Überlegungen bezüglich der Toolauswahl. Als Quellen möglicher Tools wird zwischen kommerziellen, Open-Source und eigenentwickelten Tools unterschieden werden.
Bevor es an die Evaluierung möglicher Tools geht, muss im Tool-Auswahlprozess spezifiziert werden, welche Anforderungen ein potentielles Tool zu erfüllen hat. Es muss für die jeweilige Unternehmung ein Mehrwert entstehen, indem das Tool letztlich zum Produkt, dem Projekt bzw. den zu erwartenden Projekten, den vorhandenen Skills und den Anforderungen aus Standards und Prozessen passt. Unabdingbar ist die Bereitstellung passender Methoden für die technische Durchführung der Tests und die Dokumentation der Test Nachweise, sowie der definierten Metriken (z.B. Testfortschritt, Abdeckungsanalysen, Testeffizienz).
Sind alle Auswahlkriterien spezifiziert, geht es im nächsten Schritt an die Evaluierung möglicher Tools (kommerziell, Open-Source oder eine Eigenentwicklung) und deren Ansätze. Entscheidend ist:
„Ab welchem Punkt kommt man besser mit dem einen oder dem anderen Ansatz zurecht, um das Testziel zu erfüllen?“
Der Grad der Komplexität rechtfertigt nicht immer den Toolkauf, unabhängig davon ob es ein kommerzielles oder ein Open Source Toolset ist. Ein minimalistischer Ansatz ist innerhalb eines intelligenten Testkonzepts denkbar bzw. integrierbar. Ist die zu testende Plattform „klein“ genug, um das Testen auf eine einheitliche Basis zu stellen, dann ist der passende Ansatz “Eigenentwicklung/ maßgeschneiderte Lösung” denkbar.
Bevor wir die Vor- und Nachteile der unterschiedlichen Ansätze gegenüberstellen, zeigen wir zwei Beispiele aus der Praxis. Die Eigenentwicklung der HIMA Paul Hildebrandt GmbH speziell für die HICore Plattform und die bei der tecmata GmbH angewendete kommerzielle Test-Software “Tessy.”
Mit beiden Ansätzen wurde die im nächsten Kapitel beschriebene beispielhafte Funktion sowohl gegen spezifiziertes, als auch auf nicht spezifiziertes Verhalten getestet.
Obwohl sie im Kontext funktionaler Sicherheit immer mitschwingt: die Tool-Qualifikation behandelt dieser Beitrag nicht, weil dies den Rahmen schlichtweg sprengen würde. Fragen wie: “Kann der Eigenbau normative Anforderungen erfüllen? Wenn ja, wie?” “Wie valide und langlebig ist die Safety-Zertifizierung kommerzieller Tools?” “Welche Kosten und Aufwände bringen die Qualifizierung der Eigenentwicklung mit sich?“ – sehen wir als Anregung für einen gesonderten Beitrag, zu dem wir hiermit gerne aufrufen.
Beispielfunktion für den Software Test
Wir testen eine Funktion WriteUart(unsigned char byteValue), die ein übergebenes Byte gefolgt von ‚\n‘ ausgibt und dazu die folgenden Schritte in der angegebenen Reihenfolge ausführen muss. Dieses Beispiel ermöglicht einen praxisnahen Vergleich der beiden Testansätze.
- Schreibe 0x0A in UARTAR,
- Schreibe den übergebenen Wert byteValue in UARTDR,
- Schreibe den character ‘\n’ in UARTDR,
- Schreibe 0x00 in UARTAR.
Welche Funktion die Register UARTAR und UARTDR am Ende haben ist für dieses Beispiel irrelevant.
3. Tests mit Eigenentwicklung
Im Vergleich zu einer kommerziellen Lösung müssen bei Eigenentwicklungen zusätzliche Aufwände geplant werden. Eine Eigenentwicklung kann sehr gut auf Sonderwünsche eingehen und ausgesprochen leichtgewichtig sein, um den Eigenschaften der zu testenden Plattform gerecht zu werden. Sie darf jedoch nicht den Fokus verlieren und zu einem generischen Testtool wachsen, es sei denn dies ist explizit erwünscht.
Bevor ein eigens entwickeltes Testframework tatsächlich eingesetzt werden kann, sind umfangreiche Vorarbeiten zu erledigen die nicht zu unterschätzen sind bzw. in der Aufwandschätzung und der Entscheidungsfindung beachtet werden müssen. Zu den Tätigkeiten, die im Bereich „Safety“ durchgeführt werden müssen, gehört eine ausreichende Dokumentation und eine Tool-Qualifikation zur Verifizierung und Validierung dieses Testframeworks.
Zu den Anforderungen an eine Eigenentwicklung kann die Vereinheitlichung von verschiedenen Ansätzen gehören, die im Team oder Unternehmen über die Jahre entstanden sind. Typischerweise identifiziert man Best Practices und implementiert diese im Testframework. Eine Vereinheitlichung n den richtigen Stellen kann die Testautomatisierung für hoch spezialisierten Plattformen stark vereinfachen. Bei der im Folgenden beschriebenen Eigenentwicklung gibt es für den HICore 1 (ein SIL3 zertifizierte SoC der HIMA und Vorreiter des HICore 2) eine weitere Anforderung. Diese Anforderung ist im Bereich Safety allgegenwärtig, ohne systematischen Ansatz aber recht aufwändig zu erfüllen: “Das Aufdecken von nicht-spezifizierten Verhalten”.
Bei dem gewählten Lösungsansatz steht der Wunsch “nicht-spezifiziertes Verhalten implizit aufzudecken” im Vordergrund. Also ohne zusätzlichen Aufwand wie das explizite Prüfen, dass das Testobjekt eine gewisse Aktion eben nicht durchgeführt hat. Bei einem SoC mit einer großen Anzahl an Schnittstellen und Bibliotheksfunktionen ist der Aufwand einer expliziten Prüfung hoch. Besonders möchten wir erwähnen, dass neben dem reinen zeitlichen Aufwand noch belastenden Faktoren für Tester und Reviewer hinzu kommen, die mit einer langwierigen, monotonen und fehleranfälligen Tätigkeit konfrontiert werden.
Die umgesetzte Eigenentwicklung löst dieses spezielle Problem mit Hilfe von Log Einträgen, die einen Zeitstempel und eine Nutzlast aufnehmen. Jeder Test-Dummy, der eine Aktion des Testobjektes erfasst, erstellt einen solchen Log-Eintrag der bei der Instanziierung automatisch einen Zeitstempel erhält und vom Test-Dummy mit einer Nutzlast konstruiert wird.
In jeder Assert-Phase einer Testfall Implementierung muss jetzt die Nutzlast jedes (!) Log-Eintrags der während der Testdurchführung instanziiert wurde, gegen einen Erwartungswert geprüft werden. Wird ein Log-Eintrag nicht ausgewertet, schlägt der Testfall automatisch fehl.
Derjenige, der die Tests implementiert braucht nur zu prüfen, ob das Testobjekt genau die Aktionen durchführt die auch spezifiziert sind. Führt das Testobjekt noch andere Aktionen durch, schlägt der Testfall aufgrund der nicht erwarteten und somit auch nicht geprüften Log-Einträge automatisch fehl. Zusätzlich ergibt sich Dank der Zeitstempel die Möglichkeit z.B. im Simulator, die genaue Reihenfolge von Register- und Funktionsaufrufen einfach zu verifizieren.
Unter https://gitlab.com/grischagoebel/timestamplogentries ist die Beispielimplementierung für diesen Beitrag zu finden.
Im Folgenden wird auf die Inhalte der main.cpp eingegangen, um die Prinzipien zu verdeutlichen und den Einstieg in den Code zu erleichtern.
Nach jedem Testfall wird geprüft, ob alle Log-Einträge ausgewertet wurden und die Mocks der Special Function Register UARTDR und UARTAR werden zurückgesetzt. Da in dieser Implementierung jeder Test-Dummy seine eigene Liste mit Logeinträgen führt, wird hier einfach nur diese Liste gelöscht:
Die Mocks von UARTDR und UARTAR erstellen in diesem Beispiel bei Schreibzugriffen einen Logeintrag. Die Funktion der Logeinträge wird hier noch einmal gezeigt:
Die Funktion bool Logentry::Equals(T payload) vergleicht die Nutzlast mit einem beliebigen Erwartungswert und gibt das Ergebnis des Vergleichs zurück. Dabei gilt der Log-Eintrag nach Aufruf der Methode immer als ausgewertet und PerformFinalCheck() meldet kein nicht-spezifiziertes Verhalten. Selbst wenn der Vergleich wie in Zeile 39 fehlschlägt. Es ist Aufgabe des Makros TEST_ASSERT(condition) den Testfall bei einem falschen Vergleich scheitern zu lassen.
Abbildung 3 zeigt die Möglichkeiten, die sich aus den Zeitstempeln der Log Einträgen ergeben:
Das Makro TEST_SEQ_LOG_EQUALS(logentry, payload) in Zeile 57 prüft neben der Nutzlast, ob der Zeitstempel des übergebenen Log-Eintrags größer ist, als beim vorherigen Aufruf des Makros (Zeile 56). Handelt es sich um den ersten Aufruf nach einem Aufruf von START_SEQUENCE(), wird der Zeitstempel nicht verglichen sondern nur als t0 der im Folgenden geprüften Sequenz hinterlegt.
Die zweite Sequenz ab Zeile 61 schlägt fehl, da logFirst vor logSecond instanziiert wurde und daher einen kleineren Zeitstempel als logSecond hat.
TestCase_Log_3 ist ein Beispiel für einen nicht ausgewerteten Logeintrag, der den Abschlusstest scheitern lässt:
Abschließend ein aus der Anwendung stammendes Beispiel anhand der in Kapitel 2 spezifizierten Funktion:
Ein möglicher Testfall zum Schreiben von 0x07 unter Verwendung von Log Einträgen sieht wie folgt aus:
Die Funktion GetLog(size_t index) gibt dabei den n-ten Log-Eintrag in der Mock internen Liste zurück. Hier bietet es sich an, je nach Bedarf, weitere Algorithmen wie z.B. „n-ter Logeintrag mit Nutzlast x“ bereitzustellen, um die Testimplementierung robust gegen Implementierungsdetails zu machen.
In der Implementierung von WriteUart existieren Kommentare, wie man den Testfall auf verschiedene Arten scheitern lassen kann. Unter anderem indem man einen weiteren Registerzugriff hinzufügt, der nicht spezifiziert ist.
4. Tests mit externem Tool
Im Vergleich zu einer Eigenentwicklung kann ein dediziertes Testtool praktisch ohne Zusatzaufwand in Betrieb genommen werden.
Unsere Analyse und praktische Erfahrung zeigt, dass man von den Tools einige Komfortfunktionen geboten bekommt, die bei einer Eigenentwicklung abseits der eigentlichen Kernfunktionalität entweder den Aufwand steigern oder nicht enthalten sind: z.B. automatische Analyse des Quellcodes samt Abhängigkeiten, Codemetriken, Metriken zur Testabdeckung und standardisierte Methoden zur Erzeugung fast beliebig detaillierter Reports inklusive Testfallbeschreibung.
Meistens unterstützen derartige Tools zwei Typen von Tests: Unit Tests und Szenario Tests.
Unittests
Wert zurück, haben jedoch keine eigenen Seiteneffekte auf die Software. Genauso werden auch Hardware-Ressourcen, in unserem Beispiel das UARTRD – Register, durch einfache Software Variablen ersetzt. Automatisch erzeugte Stubs geben typisch nur einen Wert zurück, manuelle dienen beispielsweise dazu, dass eine solche Funktion bei mehrfachen Aufrufen jeweils unterschiedliche Werte zurückgeben.
Jeder Testfall besteht in diesem Vorgehen aus den Eingangswerten der Funktion, den Rückgabewerten der Stubs sowie den Werten, die auf alle Ausgangsdaten der Funktion geschrieben werden. Zusätzlich kann geprüft werden, welche anderen Funktionen mit den jeweiligen Argumenten aufgerufen werden, und in welcher Reihenfolge dies geschieht. Über die Abdeckung aller derartigen Ausgänge hinweg steht als Ergebnis die Aussage, ob die interne Funktionsweise der Erwartung entspricht. Über die Prüfung aller tatsächlichen Seiteneffekte und Aufrufe ist zudem sichergestellt das es keine ungeplanten Seiteneffekte gegeben hat.Unittests haben das vorrangige Ziel, das Verhalten einzelner Code Funktionen zu prüfen. Der wichtigste Unterschied bei diesem Ansatz gegenüber dem gerade vorgestellten ist, dass alles außerhalb der aktuell getesteten Funktion entweder vollautomatisch durch das Tool oder – für mehr Flexibilität und Kontrolle mit eigener Programmierung – durch Stubs ersetzt werden. Stubs liefern lediglich einen
Unit Tests sind hervorragend dafür geeignet, die Reaktion auf verletzte Grundannahmen zu testen. Im klassischen Fall des null-Pointers würde dies in der Regel mit Tests der vollständigen Software nicht auffallen, da diese die Schnittstelle korrekt benutzt. Über Unittests kann sichergestellt werden, das die Reaktion auf den „Fall, der nicht auftreten darf“ trotzdem stimmt.
In der Beispielfunktion oben wäre so ein Fall, wenn der Stop Code „‚\n‘“ der als Stop Code in das UARTDR Register geschrieben wird, nicht als Datenwert verwendet werden darf. Während dies durch die Aufrufer bereits sichergestellt sein kann, würde eine sicherheitskritische Implementierung immer noch verpflichtet, den Fall auf eine definierte Art zu behandeln.
Nach jedem Lauf der Tests kann zusätzlich analysiert werden, ob alle Funktionen der Software getestet wurden, welche Teile jeder Funktion von den Testdaten abgedeckt werden, ob es ungetestete Pfade gibt und vieles mehr. Damit ist ggf. auch schon der Nachweis der Testabdeckung (siehe „Code und Anforderungsabdeckung“) erbracht – sofern das verwendete Tool selbst eine entsprechende Zertifizierung hat.
Integrationstests
Integrationstests (auch “Szenario Tests”) erweitern das Vorgehen der Unit Tests auf komplexere Vorgänge. Sie kommen dort zum Einsatz, wo größere Sourcecode Teile zusammen getestet werden sollen. Insbesondere kann so die Integration mehrere Funktionen oder Module als Ganzes getestet werden. Typisch werden mit dieser Methode Software-Anforderungen verifiziert.
Die besondere Stärke dieser Methode liegt darin, dass mit einem simulierten Systemtakt zyklische Aufrufe simuliert und getestet werden können. So wird die Funktion über mehrere Zyklen hinweg getestet, wobei die Ergebnisse jedes Zyklus geprüft werden können.
Grob gesagt finden die reinen Unittests Programmierfehler, bei denen isolierte Codestellen das falsche Ergebnis liefern oder falsch auf Grenzwertprobleme reagieren. Während dies prinzipiell auch von Szenario Tests entdeckt wird, ist deren Aufgabe eher, Verstöße gegen Design- und Architektur-Vorgaben aufzudecken. Die Erfahrung zeigt, dass es sinnvoll ist, beide Testarten zu verwenden und die Test-Ziele für beide Methoden festzulegen. Alles in einem testen zu wollen führt meistens zu Lücken in der Abdeckung.
Im Beispiel mit dem UART wäre der Unterschied, dass ein Unittest die korrekte Behandlung eines Zeichens belegt, der Szenariotest dagegen, dass ein ganzer Puffer Zeichen für Zeichen korrekt gesendet wird.
Code- und Anforderungsabdeckung
Wird Software nach IEC 61508 entwickelt, muss je nach zu erreichendem SIL diese zwingend durch Unit-Tests verifiziert werden. Die Anforderungen an die Code-Abdeckung nimmt mit steigendem SIL Level zu. [DIN EN 61508-3:2011-02; Tabelle B.2 Zeile 7a-d und Verfahren gemäß Anhang C.5.8]
Die geforderte Abdeckung variiert von der einfachen Anweisungsüberdeckung, über die typische Zweigüberdeckung, bis hin zu MC/DC für besonders kritische Fälle bzw. hohe SIL.
Unit Tests stellen sicher, dass einzelne Code Funktionen korrekt auf ihre Inputs reagieren und die Mechanismen zur Robustheit wie Absicherung gegen falsche Parameterwerte funktionieren. [DIN EN 61508-3:2011-02; Abs. 7.4.7 Anforderungen an den Test von Softwaremodulen; Abs. 7.4.7.2 Diese Verifikation muss zeigen, ob jedes SW Modul seine bestimmungsgemäße Funktion und keine nicht bestimmungsgemäße Funktion ausführt]
Die Testdurchführung samt Ergebnissen und der erzielten Abdeckung muss nachweisbar sein. Die Abdeckung bezieht sich nicht nur rein auf Metriken, dahinter steht die normative Anforderung: Vollständigkeit hinsichtlich der Sicherheitsbedürfnisse, die durch die Software angesprochen werden. Diese Anforderungen haben Einfluss auf die zu erzielende Abdeckung und die dazu eingesetzten Methoden. Nicht zu vergessen die normative Anforderung: Korrektheit hinsichtlich der Sicherheitsbedürfnisse, die durch die Software angesprochen werden. Spezialisierte und – im Optimalfall – vorzertifizierte Tools erleichtern diesen Nachweis, da die Funktionsweise der Tests und der Ergebnisse als vertrauenswürdig gilt. [DIN EN 61508-3:2011-02; Tabelle A.5 Zeile 8 und Verfahren gemäß Anhang C.4.7; C.5.8]
Wichtig bei der Arbeit mit Abdeckungs-Metriken ist, dass sie im normativen Kontext stets eine Minimalforderung geben. Es gibt genügend Fälle, in denen die geforderte Abdeckung erreicht ist, obwohl der Test noch nicht ausreicht, um die Funktion zu bestätigen. Ein plakatives Beispiel wäre ein simples “return x + 1;”. In dieser trivialen Funktion würde ein einzelner Input sofort dazu führen, dass die Abdeckung durch den Test gegeben ist. Für einen vollständigen Test wäre hier mehrere Tests an den Bereichsgrenzen für Über- und Unterläufe notwendig. Als Merksatz bleibt hier stehen: Metriken messen, ob der Test fertig sein kann – niemals ob er fertig ist.
Bei dem eigenentwickelten Testframework liegt der Testabdeckungsgrad nicht zwingend im Vordergrund. Der Einsatz von entsprechenden Code-Coverage-Tools muss dennoch möglich sein.
5. Die Testansätze im Vergleich
Anhand dieser Überlegungen muss letztlich jedes Team den passenden Weg finden. Es gibt weder den allein selig machenden Ansatz, noch “richtig” oder “falsch” anhand einer einfachen Checkliste. Die folgenden Abschnitte halten jeweils ein Plädoyer für einen der Ansätze – das Urteil überlassen wir dabei ganz dem Leser!
Pro Eigenentwicklung
Bei einer vergleichsweise kleinen Plattform mit überschaubarem Anwendungsgebiet und entsprechend absehbaren Anforderungen in der Zukunft, ist eine Eigenentwicklung eine ernste Alternative. Gerade die Absicherung gegen nicht-spezifiziertes Verhalten ist mit einer maßgeschneiderten Lösung am einfachsten zu erreichen.
Die Mitarbeiter arbeiten in einer vertrauten Umgebung mit denselben Tools wie bei der Funktionsentwicklung, eine Abneigung gegen den Kontextwechsel “in den Test” baut sich so gar nicht erst auf. Und wenn sich aus der Erfahrung heraus Lücken zeigen: mit der eigenen Umgebung kann man sie sofort schließen. Entwickler können sich direkt mit “Ihren” Tests identifizieren – und werden sie ausführen und Qualitätsvorgaben können individuell umgesetzt werden.
Im Vergleich zu Open Source Lösungen muss keine Rücksicht auf Lizenzmodelle genommen werden, die zum Beispiel zum veröffentlichen des Source-Codes zwingen, sollte dieser erweitert werden.
Pro Open Source
Die wenigsten Problem in der Software-Welt hat man selbst erfunden. Es gibt genug freie und offene Testframeworks von klein bis groß – das Rad muss nicht neu gefunden werden. Für nahezu jedes Problem gibt es schon eine Lösung, oder eine die nah genug dran ist und mit ein paar eigenen Erweiterungen passend gemacht werden kann.
Informationen und Beispiele finden sich in der Regel schnell. Für – im positiven Sinn – “normale” Probleme muss man sich nur hineindenken, aber weder kaufen noch selbst entwickeln.
Die Frameworks berücksichtigen teilweise bereits Qualitätsstandards und können nach individuellem Bedarf ergänzt werden.
Pro kommerzielles Tool
Wenn es um den Sicherheitsnachweis geht, schlägt die Stunde der etablierten kommerziellen Tools. Diese Tools, die für einen definierten SIL gemäß aktuell gültigem Zertifikat zugelassen sind, lassen sich mit überschaubarem Aufwand zur Qualifikation in den Sicherheitsnachweis einbinden. Das Zertifikat ist quasi ein “Qualitätslabel in der Toolauswahl.” Eine eigene aufwendige Qualifizierung entfällt hier, diese hat der Anbieter bereits vorgenommen. Es bleibt der Aufwand, ausreichende und geeignete Testspezifikationen zu erstellen, um die Software zu validieren plus die Aufwände, Testfälle bereit zu stellen, welche Tool-Fehler aufdecken.
Wenn das nächste Projekt größer wird, kann das Werkzeug damit vermutlich schon umgehen. Ein Report mehr hier, eine Metrik dort – wenn überhaupt Tests gemacht werden, wachsen die Begehrlichkeiten schneller als man selbst weiter entwickeln könnte. Andersherum besteht auch nicht die Gefahr, dass Entwickler lieber die Tests ausbauen als die eigentliche Software voran zu treiben.
Bei Anwendungs- oder Anpassungsproblemen und bei Schulungsbedarf bekommt man, abhängig vom erworbenen Lizenzmodell, professionellen Support.
Fazit
Es gibt nicht den einen richtigen Weg – nur gar nicht testen, das ist in jedem Fall die falsche Ausrichtung! Kein Softwareprodukt hat es verdient, ungetestet zu bleiben und i.d.R. ist Testen verpflichtend.
Die Autoren
Die Autoren haben ihre Erfahrungen zusammengetragen, um Herangehensweisen, eingesetzte Tools, Methoden und Skills, anhand unterschiedlicher Perspektiven und Annahmen, aus der Projektpraxis zu zeigen.
Grischa Göbel
ist bei der Yatta Solutions GmbH an Softwarelösungen aller Art beteiligt und hat in den vergangenen Jahren im Bereich funktionaler Sicherheit im Schwerpunkt Modul- und Integrationstests gearbeitet. Dabei hat er das hier in Teilen vorgestellte Test Framework entwickelt.
Vera Gebhardt
leitet die tecmata GmbH in Wiesbaden und begleitet Projekte im Bereich funktionaler Sicherheit. Sie pflegt und optimiert sowohl interne als auch externe Prozesse für tecmata und deren Kunden. Dem ASQF steht sie seit vielen Jahren als Fachgruppenleiterin Safety & Security zur Seite.
Christian Gießelbach
ist bei der tecmata GmbH Projektleiter im Bereich Test von Embedded Software. Er ist verantwortlich für die Teststrategie und die Implementierung von Testframeworks zur Steuerung von Integrationstests für funktional sichere Produkte.
0 Kommentare