Von der Theorie zur Praxis: Unit-Testing für Embedded-Software
Unit-Testing ist ein gut verstandenes Konzept, da es für jede Programmiersprache entsprechende Frameworks und umfangreiche Fachliteratur gibt. In einem modernen Entwicklungsprojekt gehört es zu den gängigen Qualitätssicherungsmassnahmen. Unbestritten ist, dass Unit-Testing die Code-Qualität steigert und beispielsweise durch Regressionstests das Fehlerrisiko mindert. Natürlich sind im Projekt noch weitere Testmethoden notwendig, unter anderem statische Codeanalyse sowie Integrations- und Systemtests. Die Testing-Trophy (siehe Abbildung 1) visualisiert, dass jede Testmethode ihre Notwendigkeit hat und die Aufwände entsprechend skaliert werden müssen. Unit-Tests lassen sich beispielsweise leichtgewichtig erstellen, idealerweise zusammen mit der Codeentwicklung, weil der Scope klein ist (die Unit) und – bei gutem Design – die Abhängigkeiten gut durch Mocks ersetzt werden können.
Trotzdem stellen wir in Embedded-Entwicklungsprojekten häufig fest, dass Unit-Testing entweder gar nicht oder nur sehr sporadisch eingesetzt wird. Woran könnte das liegen? Hier sind einige mögliche Gründe und wie wir bei CSA darauf reagieren:
Es gibt so viele Unit-Test Frameworks, welches ist das Beste?
Wir haben sehr gute Erfahrungen mit Google Test und Google Mock gemacht. Diese Frameworks bieten leistungsstarke Funktionen, sind ausgezeichnet dokumentiert, weit verbreitet und werden kontinuierlich weiterentwickelt.
Auf dem Target ist Unit-Testing umständlich und langsam.
Ja, das stimmt. Wir sind aber der Überzeugung, dass der Grossteil des Applikationscodes auf dem Entwicklungs-PC, also unter Windows oder Linux getestet werden sollte. Hier ist die Ausführung schnell, man kann einfach Debuggen und weitere Features wie Code-Coverage sind verfügbar. Ausserdem lässt sich die Automatisierung mit CI-Tools leicht umsetzen.
Der Code hat zu viele Abhängigkeiten und kann nicht getestet werden
Könnte es sein, dass Architektur und Design vernachlässigt wurden und der Code «gewachsen» ist? Moderne Microcontroller haben in der Regel grosszügigen Flashspeicher. Die Verwendung von Interfaces und Methoden wie Dependency-Injection erhöhen nicht nur die Testbarkeit, sondern führen auch zu verständlicherem Design und portablerem Code. Wir verwenden hierfür unser Building-Block-Framework, das solche Aspekte bereits berücksichtigt.
Hardwarenaher Code ist schwierig mit einem Unit-Test zu testen
Das ist korrekt. Unit-Tests sind für hardwarenahen Code selten die beste Wahl. Für jede Aufgabe soll das passende Tool und die richtige Testmethode eingesetzt werden. HAL-Libraries konnten wir erfolgreich und effektiv mit Integrationstests testen.
Wie soll ein C-Projekt mit Google Test und Google Mock getestet werden?
Die C-Header müssen auch im C++ Test-Code eingebunden werden können. Für den Mock braucht es noch zusätzlichen Glue-Code, er lässt sich dann aber wie gewohnt verwenden. Mit diesem Ansatz haben wir bereits SIL-4 Projekte im Bahnumfeld erfolgreich getestet.
Der Compiler unterstützt meine Unit-Testing Library nicht
Das muss er auch nicht. Ein gutes Design kapselt HW- und Compiler-spezifischen Code. Der Grossteil des Codes ist durch die Verwendung von C und C++ portabel und kann auf dem Entwicklungs-PC mit einem geeigneten Compiler getestet werden. Mit diesem Ansatz haben wir immer sehr gute Erfahrungen gemacht.
Das Projekt enthält keine Unit-Tests – warum sollten nun welche geschrieben werden?
Wenn der neu entwickelte Code Unit-Tests hat, ist der Anfang gemacht. Wir haben Unit-Testing erfolgreich in bestehende Projekte integriert und konnten die Testabdeckung schrittweise erhöhen.
Das Unit-Testing Framework ist im Projekt nicht integriert
Wie man ein Unit-Test Framework einbindet, ist gut dokumentiert und einfach zu bewerkstelligen. Diese Ausrede gilt also nicht. Mit unserer embedded Toolchain haben wir eine nahtlose Integration aller notwendigen Tools realisiert. Einen Unit-Test zu schreiben ist damit fast einfacher, als lauffähigen Code auf dem Zielsystem zu haben.