Unit Tests sind im PC Bereich mittlerweile Standard. Allerdings ist es schwierig sie auch für Software im Bereich der Mikrocontroller zu verwenden. Die wenigen Ressourcen, die der Mikrocontroller zur Verfügung stellt sollten idealerweise für die Software verwendet werden, die später dann auf dem System laufen soll, denn alles andere wäre Geldverschwendung. Es gibt dennoch die Bemühungen mit so wenig Wasserkopf wie möglich Unit Tests auch auf Mikrocontrollern zu implementieren.
Ich habe das in meinen Projekten bisher so gelöst, dass ich zusätzlich zum normalen Programmcode Testfunktionen implementiert habe. Alles was nur zu Testzwecken im Code eingefügt wurde habe ich mit Präprozessor-Anweisungen eingekapselt, sodass sie bei einem Release Built nicht beachtet wurden. Dadurch habe ich während der Entwicklungszeit die Testfunktionen zur Verfügung und später im ‚fertigen‘ Projekt wurden die Testfunktionen nicht mehr übersetzt. Das führte neben kleinerem Programmcode auch zu schnellerer Abarbeitung von z.B. Interrupt Service Funktionen im fertigen Projekt. In dieser Artikelserie werde ich die Verwendung von µCUnit anhand des ASURO Projekts erklären.
Das µCUnit Framework liegt als Open Source Projekt auf GitHub und kann von dort als zip-Archiv heruntergeladen werden. Die verschiedenen Beispielprojekte für i386, avr und arm zeigen, wie das Framework verwendet werden kann.
Da jeder Mikrocontroller und jedes Projekt unterschiedlich sein kann, muss das Framework mit Macros angepasst werden. So muss zum Beispiel die Text Ausgabe so implementiert werden, dass Textzeichen über die Serielle Schnittstelle, Netzwerk, oder Dateien ausgegeben werden können. Sollte auf der Projektplattform die printf() Funktion zur Verfügung stehen, kann sie verwendet werden, aber gerade bei kleinen Controllern ist selten genügend Platz um diese Funktionen mit in dem Programmcode aufzunehmen. Es wird zusätzlich noch eine Funktion benötigt, um das System in einen sicheren Zustand zu setzten und es komplett herunter zu fahren. Jeder Ausgangspin der Hardware sollte in einen sicheren Zustand gesetzt werden, um zum Beispiel hohe Ströme durch den Controller zu verhindern. Die Beispieldateien System.c und System.h enthalten Code, der diese Funktionen beschreibt.
Die System.c für den AVR zeigt welche Funktionen vom Framework erwartet werden. Vor allem die Abwesenheit der printf() Funktion muss behoben werden. Da der ASURO schon eine schlanke Funktion zum Ausgeben von Zeichen besitzt, muss hier nur die Funktion zum Schreiben von Strings implementiert werden:
void System_WriteString(char * s) {
while(*s)
{
UARTbyte(*s);
s++;
}
}
Alle weitern Funktionen habe ich direkt aus der Beispieldatei übernommen.
Nachdem die Funktion implementiert ist, könne wir mit dem Aufbau eines Testcases beginnen. Dazu können wir uns an der mitgelieferten Testsuit.c orientieren. Eine Test Suite ist aufgebaut aus dem Haupt Test und den einzelnen Testcases. Der Haupttest initialisiert den Prozessor und führt die einzelnen Tests durch.
Hier wird auch die int main(void) implementiert; die Funktion, die als Hauptfunktion aufgerufen wird.
Das Beispiel zeigt und diese Funktion
int main(void)
{
UCUNIT_Init();
// [...]
Testsuite_RunTests();
UCUNIT_WriteSummary();
UCUNIT_Shutdown();
return 0;
}
Die Funktionen UCUINT_Init() und UCUNIT_Shutdown() sind in der System.c schon implementiert und tun zur Zeit nichts. Die Funktion Testsuite_RunTests() ruft die einzelnen Tests auf:
void Testsuite_RunTests(void)
{
Test_BasicChecksDemo();
Test_PointersDemo();
Test_ChecklistDemo();
Test_BitChecksDemo();
Test_CheckTracepointsDemo();
}
Es werden alle Tests in der Testsuite aufgerufen. Diese Herangehensweise erlaubt es die Tests einzelner Funktionesteile (Testcases) in verschiedene Hauptkategorien (Testsuits) zu unterteilen. Somit erhält man leichter einen Überblick über die einzelnen Funktionstests.
Schauen wir uns zunächst die einzelnen Testcases an. Der BasicCheckDemo() Test führt ein paar grundlegende arithmetische Operationen aus. Alle diese Tests sollten bestanden werden. Zu Beginn eines jeden Testcases werde die darin verwendeten Variablen deklaiert und eventuell initaialisiert. Der nächste Schritt ist den Testcases zu beginnen, mit Hilfe der Funktion
UCUNIT_TestcaseBegin("Name des Testcases");
Danach kommt dann eine eventuelle Generierung von Testdaten oder sonstige Funktionalität. Danach werden die Ergebnisse der Funktionen evaluiert. Hier sieht man die Verwendung der CheckIsEqual Funktion, die wie ihr Name schon sagt, zwei Werte miteinander vergleicht. Am Ende jedes Testcases wird die Funktion
UCUNIT_TestcaseEnd();
aufgerufen um den Testcase zu beenden und das Ergebnis zu speichern.
Weiter Testfunktionen werden in den anderen Testcases vorgestellt. Ich möchte an dieser Stelle noch einmal auf die Evaluierungsfunktion CheckIsBitSet eingehen, da hier direkt die Ausgangspins des Controllers getestet werden können.
PORTB = (PB1 << 1);
UCUNIT_CheckIsBitSet(PORTB, 1); /* Pass */
Im der Nächstem Artikel betrachten wir dann die einzelnen Funktionen des ASURO und wie wir eine Testsuite für die einzelnen Hauptgruppen und Testcases für alle Unterfunktionen entwickeln.