AdTech Ad szmtag
Anzeige
Anzeige
elektroniknetMarkt & TechnikElektronik DESIGN & ELEKTRONIKComputer & AUTOMATIONElektronik automotive

Anzeige

Unit-Tests mit Open-Source-Werkzeugen

Qualitätssicherung für Embedded-Linux-Projekte

Von Roland Stigge

In der Open-Source-Welt existieren viele Tool-Suiten, die für übliche Build-Umgebungen genutzt werden. Bekannte Vertreter sind make und autotools, die zunehmend auch für qualitätssichernde Maßnahmen eingesetzt werden. Das Ziel ist die Integration von Unit-Tests und generellen Testverfahren mit auf den Einsatzzweck abgestimmten Eigenschaften.


Anzeige

Tests sollten so weit automatisiert werden wie nur möglich. Andernfalls werden Entwickler diese in ihrer alltäglichen Arbeit nicht nutzen. Der Aufwand für das Schreiben von Testroutinen sollte nicht als unnötiger Overhead angesehen werden. Die Durchführung der Tests muss für den Entwickler einfach genug zu bewerkstelligen sein, dass sie/er den Prozess als eine Erleichterung versteht und weniger als Zeitverlust. Deshalb sind schnelle und bequem durchzuführende Läufe von „make test“ auch in immer mehr Open-Source-Paketen zu finden. Die Implementierung von Test-Code sollte als integraler Bestandteil eines Projekts oder Pakets angesehen werden. Das ist der Fall, wenn während einer Projektentwicklung parallel am Test-Code gearbeitet wird. Idealerweise wird dieser im selben Code-Repository vorgehalten und gepflegt, so dass jeder Entwickler und andere mit dem Projekt befasste Personen Zugriff darauf haben und ihn nutzen können.

Bild 1. Die Übersicht über den Test-Prozess zeigt, wie aus den Quellcode-Dateien (code.c, libcode.c und testcode.c), configure.ac und Makefile.am die Programme übersetzt werden. Autoscan erstellt automatisch eine erste Version von configure.ac. In der Umgebung des Package-Maintainers (grüner Hintergrund) werden mit Hilfe von autoconf, automake, autoheader, aclocal und libtoolize (rote Umrandungen) einige Dateien generiert. Dies kann mit autoreconf automatisiert werden. Beim Übersetzen für das Zielsystem kann der Benutzer (blauer Hintergrund) ./configure aufrufen, womit config.status und damit die Makefiles erstellt werden; „make“ nimmt dann das Makefile auf und steuert cpp (Präprozessor), gcc (C-Compiler), ld (Linker), und libtool (Bibliothekserzeugungsabstraktion), um die eigentliche Übersetzung (zu program, testprogram, libcode.so) vorzunehmen.

So wird es sogar möglich, Test-Suites vollautomatisch ablaufen zu lassen, etwa in Form eines „nightly test“, wie man es sonst von den „nightly builds“ her kennt. Für praktisch jede Programmiersprache gibt es wenigstens ein etabliertes und frei erhältliches Unit- Test-Framework. Vertreter sind u.a. JUnit für Java, CUnit und autounit für C und PyUnit für Python. Die allen gemeinsame Funktionalität liegt in der Sammlung von Testfällen mit dem dazugehörigen Test-Code, der vom jeweiligen Entwickler bereitgestellt wird, und in einem standardisierten Verfahren für den Auf- und Abbau einer Testumgebung (Harness).

Die Anwendung von Tests ist nicht beschränkt auf Funktionen und Methoden. Sie können auch auf höhere Ebenen ausgedehnt werden, indem z.B. Tests mit Skript-Sprachen wie Shell, Perl und Python implementiert werden, die direkt die Features der Entwicklungsumgebung mitbenutzen können. Auf diese Weise kann eine automatisierte Test-Suite verschiedene Ebenen abdecken von Funktionen und Methoden über Bibliotheken bis hin zu Benutzeroberflächen.

Bei der Entwicklung von Embedded- Software, besonders unter Linux, muss besondere Aufmerksamkeit bei der erfolgreichen Anwendung der vorgenannten Methoden verwendet werden. Diese können – verallgemeinernd gesprochen – in der Embedded-Entwicklung in der gleichen Art und Weise verwendet werden wie in der allgemeinen Software-Entwicklung, allerdings mit ein paar zusätzlichen Eigenschaften wie z.B. Cross-Compilierung und mit Ausführen von Test-Code auf einem Remote-System.

Das generelle Ziel von Unit-Test ist die (voll)automatische Erkennung von Fehlern und Problemen in der Embedded- Software. Es geht also um die Maximierung der Qualität bei gleichzeitiger Minimierung der Testaufwendungen. Durch die Integration von Software- Tests in einem hohen Maße zu einem frühen Zeitpunkt des Entwicklungsprozesses können umfangreiche manuelle Tests zu späterer Zeit deutlich verringert werden.

Besser vorhersagbarer Projektverlauf durch Qualitätssicherung

Qualitätssicherung oder „QA“ (Quality Assurance) ist bei Betrachtung heutzutage üblicher Software-Entwicklungsprojekte deutlich unterrepräsentiert. Dies gilt umso mehr für Open- Source-Projekte. Das ist für die Branche aber typisch, wenn man die Projekte auslässt, für die eine enge Einbindung qualitätssichernder Maßnahmen bereits während der Entwicklung zwingend erforderlich ist, z.B. in der Luft- und Raumfahrt, der Medizintechnik oder der Kernkraft.

Dieser Artikel konzentriert sich auf das so genannte „Unit Testing“ als ein wesentliches Element zur Verbesserung von Software-Qualität. Unit-Tests erlauben es dem Programmierer, den Source-Code seines Projekts mit einer Test-Suite zu ergänzen, die weitgehend automatisiert abläuft. Zudem wird die Schwelle, immer und immer wieder Tests an jeder Stelle im Entwicklungsprozess zu wiederholen, deutlich herabgesenkt, und es wird für eine bessere „Vorhersagbarkeit“ des Projektablaufs gesorgt. Anstelle einer schnellen Entwicklung mit hohem Zeitaufwand bei der Fehlerbehebung kann sich der Software-Entwickler nun mehr Zeit für die Definition der Funktionalität nehmen, die dann automatisch von der Source-Code- Ebene bis zu hoher Abstraktionsebene getestet werden kann.

Andere, wichtige Bestandteile eines QA-Konzepts, die eine große Unterstützung bei der Software-Entwicklung liefern können, sind die aus Platzgründen hier nur kurz aufgelisteten Werkzeuge:

Im Folgenden wird ein Szenario für die Entwicklung eines Embedded-Software- Pakets im GNU-/Open-Source- Stil vorgestellt, das aber auch in einem proprietären Umfeld angewendet werden kann. Die Voraussetzungen hierfür sind:

JUnit – Mutter aller Unit-Test-Suiten

Die üblicherweise im Unit-Test genutzten Tool-Suiten sind abgeleitete Versionen des wohlbekannten Test- Frameworks JUnit von Kent Beck und Erich Gamma, die in den jeweiligen Programmiersprachen entwickelt wurden. Da es relativ simpel ist, dieses Framework trotz seiner Mächtigkeit in einer neuen Sprache zu implementieren, ist es mittlerweile in praktisch jeder Programmiersprache erhältlich und weitgehend akzeptiert. Mitunter gibt es sogar mehrere bekannte Implementierungen für eine einzelne Sprache in Abhängigkeit unterschiedlicher Entwicklungsumgebungen. Bekannte Vertreter sind beispielsweise: JUnit für Java, CUnit und autounit für C, PyUnit/unittest für Python sowie „Test::Unit“ und TAP für Perl.

Diese Aufstellung ist bei Weitem nicht vollständig. Es ist aber leicht, eine Version für die präferierte Sprache zu finden. Im nachfolgend beschriebenen Beispielprogramm werden, wo immer möglich, Standard-Tools der GNU-Welt genutzt. Das schließt die bekannten Frameworks wie die autotools (autoconf, automake, libtool, aclocal, autoheader) mit ein wie auch die Zusatz-Tools wie z.B. GNU autounit, make, gcc. Diese sind allgemein anerkannt und weit verbreitet in der Open-Source-Welt. Der Kasten enthält eine kurze Beschreibung dieser Vertreter.

Standard-Tools der Open-Source-Welt

make

Das Standard-Build-System unter Unix und GNU/Linux. Obwohl es Alternativen gibt, ist es das am weitesten verbreitete Framework unter Verwendung von Makefiles, die die Abhängigkeiten der zusammenzubindenden Objekte, Executables und Bibliotheken beschreiben. Im Makefile werden Abhängigkeiten zwischen Quelldateien und zu erzeugenden Objekt-, Bibliotheks- und ausführbaren Dateien angegeben, zusammen mit nötigen Befehlsfolgen zur Compilierung, zum Binden etc. Diese Abhängigkeiten werden von Make automatisch aufgelöst und führen nach dem Aufruf „make“ zum automatischen Übersetzen aller noch nicht erstellten bzw. veralteten Zieldateien.

gcc

Die GNU-Compiler-Kollektion, oftmals auch als „GNU C Compiler“ bezeichnet, obwohl gcc weitere Compiler für Fortran, Java, C++, ObjectiveC etc. beinhaltet. Es wird aber nicht nur unter GNU genutzt, sondern auch mit BSD, Solaris etc. Der wichtigste Vertreter der enthaltenen Tool-Sammlung ist zweifelsohne der C-Compiler (cc/gcc), aber ebenso wichtig sind Assembler (as/gas, zum Erzeugen des Maschinencodes), Linker (ld, zum Binden der Objektdateien zu ausführbaren oder Bibliotheksdateien), C-Präprozessor (cpp, u.a. zum Einbinden von Include-Dateien in die Quelltextdateien).

autoconf

Die Konfiguration eines portierbaren Programms (üblicherweise in C/C++ geschrieben) für eine spezifische Plattform ist eine komplizierte Angelegenheit; autoconf kann automatisch ein configure-Skript generieren, wie es für GNU/Linux-Software-Pakete üblich ist, und erzeugt die Symbole, welche einfach in die Programme eingebunden und genutzt werden können. Darüber hinaus erzeugt ein Lauf mit configure die erforderlichen Makefiles, welche an das Build-System angepasst werden. Hierbei wird autoconf vom Paket- Maintainer aufgerufen, der das configure- Skript im Source-Paket mitliefert. Die Ausführung dieses Skriptes bleibt dem Anwender des Paketes überlassen.

autoscan

Ein einfaches Tool, um eine erste configure. ac zu erzeugen, mit der man die Arbeit beginnen kann. Das ist hilfreich in Projekten, die bereits eine gewisse Komplexität im Source-Code erreicht haben und mit dem GNU-Framework erweitert werden sollen. Diese Quellen werden gescannt und mit einem heuristischen Ansatz in configure.ac behandelt. Die Ausgabe von autoscan ist eine Datei namens configure.scan, die vom Paketentwickler angepasst (Paketname, div. Parameter) werden muss, bevor sie in configure. ac umbenannt wird und offiziell zum Paket hinzugefügt wird.

automake

autoconf benutzt Makefile-Templates (Makefile. in) als Input. Diese haben einen langen und komplizierten Inhalt; automake kann dabei helfen, diese Dateien mit einem Satz einfacher Definitionen in Makefile.am zu überführen. Makefile.am listet dabei normalerweise nur die Quelldateien auf und gibt ggf. zusätzliche Compilerschalter an, die von Voreinstellungen abweichen. Daraus erzeugt automake ausführliche Anweisungen im Makefile-Format, die später vom configure- Skript leicht in Makefiles umgewandelt werden können.

aclocal

Die Eingaben für autoconf (configure.ac) sind in der Makro-Sprache M4 geschrieben. Normalerweise werden nur einige Standard- Makros genutzt. Einige, die mit automake in Verbindung stehen, befinden sich in aclocal. m4 und werden einfach mit aclocal erzeugt.

autoheader

autoconf erzeugt eine Liste mit Makro-Definitionen, die von C-Programmen (config.h) benutzt werden können. Für gewisse Fälle ist es jedoch nützlich, über eine Liste der Templates für diese Makros (config.h.in) zu verfügen. Diese wird von autoheader erzeugt. In einer idealen Welt wäre autoheader nicht nötig, aber in der Praxis ist es eine große Hilfe beim Debugging.

libtool

Ein Werkzeug, das bei der automatischen Erzeugung statischer und „shared“ Libraries hilft und hierbei von diesen unterschiedlichen Konzepten abstrahiert. Zusammen mit autoconf kann dieses die Software auf das aktuelle Target-System anpassen. Die normalerweise unterschiedliche Behandlung von statischen und shared Libraries wird hiermit dem Entwickler komplett abgenommen und führt auf dem übersetzenden System zur automatischen Erzeugung der am besten zum System passenden Bibliotheksimplementierung.

autoreconf

Ein einfaches Tool, das Programme wie autoconf, autoheader, aclocal, automake, libtoolize und andere (z.B. bei Bedarf gettextize zur Internationalisierung von Paketen) in der korrekten Reihenfolge aufruft. Üblicherweise wurde bei entsprechenden Paketen ggf. auch ein Skript autogen.sh mitgeliefert, das die genannten autotools in der richtigen Reihenfolge aufruft. Dies ist mit autoreconf für die meisten Fälle nicht nötig. autoreconf muss immer dann aufgerufen werden, wenn die jeweiligen Eingabedateien für die genannten Programme geändert wurden.

autounit

Eine C-Bibliothek als Derivat des originalen Unit-Test-Frameworks von Kent Beck und Erich Gamma. Sie ist gut mit den GNU autotools integriert und bietet die erwarteten Schnittstellen zum Auf- und Abbau von Testumgebungen (Harness), der Implementierung der Tests selbst und eine übersichtliche Aufbereitung der Testergebnisse.

Zu allen hier genannten Programmen wird eine umfangreiche Dokumentation im Manoder Info-Format mitgeliefert. Dort werden viele nützliche Detailinformationen, insbesondere alle Kommandozeilen-Parameter, genannt.

 

 

Das Aufsetzen eines Projekts oder Packages mit GNU autotools ist relativ einfach. Allerdings kann man nicht behaupten, dass es auch überall gut beherrscht wird. Hier ist es oft hilfreich, die Dokumentationen der entsprechenden Pakete aufmerksam zu studieren. Ein simples Beispiel hierfür ist im nachfolgenden Kapitel dargestellt.

Zu Beginn kann autoscan eine configure. ac herstellen, indem man diese von configure.scan umbenennt. Das Makefile.am kann so einfach aussehen:

bin_PROGRAMS = helloworld

Angenommen, es wurden eine configure. ac und einige Makefile.am erstellt, sollten im nächsten Schritt Tests für den eigentlichen Code geschrieben werden. Im Idealfall werden diese Tests sogar vor dem eigentlichen Programm- Code erstellt. Jedoch, selbst wenn man mit aller Kraft versucht, dieses Ziel zu erreichen, wird man in der Realität nicht umhin können, eine gewisse evolutionäre Dynamik im Rahmen des Entwicklungsprozesses zu berücksichtigen, die einen davon abhält, wirklich alle Tests vor der „echten“ Code-Erstellung entwickelt zu haben.

Integration der Testfälle in die Code-Basis

Zur besseren Unterstützung des Entwicklers verfügt automake bereits über einige Eigenschaften zur Integration von Tests in die Code-Basis. Das Auflisten von Executable-Dateien in der Variablen TESTS in Makefile.am macht automake auf den Umstand aufmerksam, dass bereits Tests im jeweiligen Verzeichnis lokalisiert sind. Automake wird sodann die erforderlichen Regeln für einen einfachen make check zum Zeitpunkt des Builds erzeugen. Es ist möglich, die Tests zusammen mit dem eigentlichen Quellcode des Projekts zu integrieren, beispielsweise, indem das gleiche Verzeichnis genutzt wird. Genauso gut kann der Test-Code aber auch in einem separaten Verzeichnis gepflegt werden, was die empfehlenswertere Methode darstellt. Auf diese Weise ist der Test-Code klar vom eigentlichen Programm-Code getrennt, wodurch vermieden wird, dass Test- Code mit Produktiv-Code installiert wird.

All diese Vorkehrungen treffen normalerweise nicht so sehr auf die Entwicklung von Embedded-Software zu, da hier die Programme auf einem anderen System ausgeführt werden, als das, auf dem sie entwickelt wurden. Obwohl GNU autotools eine Cross- Compilierung erkennt, beherrscht automake nicht die Ausführung eines Programms auf einem Remote-Host. Vielleicht ist dies dem Umstand geschuldet, dass Embedded-Entwicklung zu vielfältig ist, was an zahlreichen komplett verschiedenartigen Ansätzen zur Compilierung, dem Host-Target Setup und anderen Dingen zu erkennen ist. Immerhin haben die Autoren von automake eine Variable vorgesehen, die für das Setup einer Host-Target- Verbindung zu Testzwecken verwendet werden kann: TESTS_ENVIRONMENT. Inklusive des Aufsetzens von Variablen spezifiziert es den Befehl zum Ausführen des Testprogramm- Aufrufs anstelle von /bin/sh. Das ermöglicht eine Verbindung zum Target via SSH mit:

TESTS_ENVIRONMENT = ssh username@targethost

Hierbei handelt es sich um die spezielle Syntax des Makefiles zum Einbinden einer Variablen. Die konkreten Werte dieser Variablen können sich in der Realität unterscheiden, z.B. hinsichtlich Einstellungen für die entsprechende Target-Umgebung. Wichtig ist, dass das Kommando zur Ausführung als Argument bereitgestellt wird, so dass SSH das entsprechende Testprogramm auf dem Target ausführen kann.

Neben SSH, das eine Netzwerkverbindung erfordert, kann auch eine serielle Verbindung zum Target genutzt werden. Die einzigen zwingenden Maßnahmen sind das automatische Deployment des gerade compilierten Test-Codes zum Target, die dortigen Programmaufrufe und Auswerten des Return-Wertes und der Standardausgabe. Während im geschilderten Beispiel dies simpel über NFS und SSH bewerkstelligt wird, kann ebenfalls relativ einfach eine Client-Server-Infrastruktur implementiert werden, die diese Funktionalität über unterschiedliche Verbindungswege liefert. Einen weiteren Ansatz könnte eine Netzwerkverbindung mittels serieller Leitungen oder USB darstellen.

Für den Fall, dass die Dateien via NFS exportiert werden, ist es am besten, die gleiche Verzeichnis-Hierarchie sowohl für Entwicklungs-Host als auch für das Target zu wählen. So ist es nur noch nötig, auf dem Target in das gleiche Verzeichnis zu wechseln wie auf dem Host und dort dieselben Programme auszuführen wie auf dem Host.

Test-Setup am Beispiel

Nachfolgend wird ein Beispiel etwas detaillierter vorgestellt. Das Paket (hellotest-XX.XX.tar.gz) kann von der Website www.philosys.de/news/ heruntergeladen werden. Es ist ein reguläres Paket im GNU-Stil und kann – außer der Notwendigkeit, einen Cross-Compiler auf dem eigenen Entwicklungs- Host aufzusetzen – so genutzt werden, wie es ist (siehe auch die Datei „Readme“.)

Das Makefile.am im Hauptverzeichnis des Pakets besteht aus den Zeilen

SUBDIRS = src lib tests
test: check

Der Test-Code (/tests directory) ist getrennt vom eigentlichen Code (/src und /lib). Normalerweise ist check das von den GNU autotools bereitgestellte Target. Bequemerweise kann test als ein Alias definiert werden. Den Inhalt der configure. ac zeigt Listing 1. Die meisten Code-Zeilen in configure.ac sorgen dafür, dass alles an seinem richtigen Platz ist. Diese Information wird später in Makefiles genutzt, um die entsprechenden Header-Dateien, abhängige Bibliotheken und weiteres zu finden. Die letzte Definition von AC_ OUTPUT spezifiziert, welche Makefiles erzeugt werden sollen. Der Inhalt dieser Makefiles ist recht simpel. Der Makefile.am im Verzeichnis /src lautet:

bin_PROGRAMS = hellotest

hellotest_SOURCES = hello.c main.c

noinst_HEADERS = hello.h

Und lib/Makefile.am:

lib_LTLIBRARIES = libhellotest.la

libhellotest_la_SOURCES = hellotest.c

include_HEADERS = hellotest.h

AC_PREREQ(2.59e)
AC_INIT(hellotest, 1.0, stigge@philosys.de)
AM_INIT_AUTOMAKE
AC_CONFIG_SRCDIR([src/main.c])
AC_CONFIG_HEADER([config.h])

# Checks for programs.
AC_PROG_CC
AC_PROG_LIBTOOL

# Checks for libraries.
AM_PATH_GLIB

# Checks for header files.
AC_HEADER_STDC
AC_CHECK_HEADERS([fcntl.h stdlib.h string.h termios.h unistd.h])

# Checks for typedefs, structures, and compiler characteristics.
AC_HEADER_TIME

AC_MSG_CHECKING(to see if we can add ‘-Wall -W’ to CFLAGS)
if test x$GCC != x ; then
CFLAGS=“$CFLAGS -D_U_=\“__attribute__((unused))\“ -Wall -W -D_GNU_SOURCE“
AC_MSG_RESULT(yes)

else

CFLAGS=“-D_U_=\“\“ $CFLAGS“
AC_MSG_RESULT(no)

fi

# Checks for library functions.
AC_FUNC_SELECT_ARGTYPES
AC_CHECK_FUNCS([memset select strstr])

AC_OUTPUT([Makefile
src/Makefile
lib/Makefile
tests/Makefile])

Listing 1. Die meisten Code-Zeilen der Datei configure.ac listen auf, welches Element in welchem Verzeichnis zu finden ist. Am Ende spezifiziert die Definition von AC_OUTPUT, welche Makefiles erzeugt werden sollen.

Wie man sieht, ist die Defintion der Quellen und der Ziel-Datei einer Bibliothek genauso einfach wie bei einem Programm. Bis jetzt wurden nur ein Programm und eine Bibliothek unter test definiert. Wie bereits erwähnt, kann man die Tests in der Variable TESTS eines Makefile.am auflisten. Listing 2 zeigt das Makefile des Verzeichnisses tests/.

TESTS_ENVIRONMENT enthält das Kommando cd ‘pwd’, um in das entsprechende Verzeichnis auf dem Target zu gehen, bevor das eigentliche Executable ausgeführt wird. Die Testprograme sind unterteilt in Binär- Tests (hier die Variable: OUR_ BINARY_TESTS) und Skript-Tests (Variable: OUR_SCRIPT_TESTS). Das ist notwendig, um automake mitzuteilen, wie die erforderlichen Build- Regeln zu erzeugen sind. Diese Listen werden in den Variablen check_ PROGRAMS und check_SCRIPTS angegeben, um die Generierung der Regel check zu unterstützen. Im Fall von automake/autounit sind Tests in Testlisten eingeteilt, die in Makefile.am aufgelistet werden. Diese Programme können über vielfältige Testfall-Implementierungen verfügen, von denen jede einzelne in der Testumgebung ausgeführt wird.

TARGET_LOGIN=root@targetdev
TESTS_ENVIRONMENT=ssh $(TARGET_LOGIN) „cd ‘pwd’ ; „

OUR_SCRIPT_TESTS = test-test
OUR_BINARY_TESTS = test-functions test-static test-library

OUR_TESTS = $(OUR_SCRIPT_TESTS) $(OUR_BINARY_TESTS)

TESTS = $(OUR_TESTS)

check_PROGRAMS = $(OUR_BINARY_TESTS)

test_functions_SOURCES = test-functions.c
test_functions_LDADD = ../src/hello.o
test_functions_LDFLAGS = -lau-c-unit $(GLIB_LIBS)
test_functions_CFLAGS = $(GLIB_CFLAGS)

test_static_SOURCES = test-static.c
test_static_LDFLAGS = -lau-c-unit $(GLIB_LIBS)
test_static_CFLAGS = $(GLIB_CFLAGS)

test_library_SOURCES = test-library.c
test_library_LDADD = ../lib/libhellotest.la
test_library_LDFLAGS = -lau-c-unit $(GLIB_LIBS)
test_library_CFLAGS = $(GLIB_CFLAGS) -I../lib/

check_SCRIPTS = $(OUR_SCRIPT_TESTS)
noinst_SCRIPTS = $(OUR_SCRIPT_TESTS)
EXTRA_DIST = $(OUR_SCRIPT_TESTS)

Listing 2. Das Makefile.am im Verzeichnis /tests teilt sich in Abschnitte für Funktions-Tests, Library-Tests und Tests der statischen C-Funktionen.

Zum Test von Funktionen, die global sind in Bezug auf ein Objekt („nichtstatisch“), kann der Test-Code mit dem Objekt aus dem eigentlichen Quell- Verzeichnis (siehe Makefile.am) mit einigen Funktionen gebunden werden (siehe Listing 3). Wie man hier sieht, können mehrere Tests in einer Gruppe spezifiziert werden und zusammen in einer initialisierten Testsuite ablaufen. Hierbei referenziert das Präfix au_ auf autounit. In obigem Beispiel können die beiden letzten Argumente von au_new_suite() einfach auf „0“ gesetzt werden. Ansonsten wirken sie als Funktionszeiger für Code, die vor bzw. nach jedem Test Function Call aufgerufen werden sollen. Hier ist der kurze Setup-Code in den Testfunktionen enthalten. Innerhalb der jeweiligen Tests können diverse Bedingungen definiert werden, die an der entsprechenden Stelle im Code erfüllt sein müssen. Andernfalls wird der gesamte Test als FAILED gewertet. Die Resultate aller Tests werden zusammengefasst und zum Schluss angezeigt, wie später gezeigt wird.

#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <glib.h>

#include <autounit/autounit.h>

#include „../src/hello.h“

#define TTY_DEVICE „/dev/ttyX1“

gint
test_functions_tty_open(autounit_test_t *t)
{

int fd;

fd = dev_open(TTY_DEVICE);
au_assert(t, fd >= 0, „dev_open didn’t return valid file decriptor“);

if (fd >= 0) {
dev_close(fd);
}

return TRUE;
}

[...]

static autounit_test_group_t tests[] = {
{„test_functions_tty_open“, test_functions_tty_open, TRUE, TRUE},{„test_functions_tty_open_badfile“, test_functions_tty_open_badfile, TRUE, TRUE},
{„test_functions_tty_close“, test_functions_tty_close, TRUE, TRUE},{„test_functions_tty_init“, test_functions_tty_init, TRUE, TRUE},{„test_functions_tty_ok“, test_functions_tty_ok, TRUE, TRUE},
{0, 0, FALSE, FALSE}
};

int main(int argc _U_, char* argv[] _U_) {
autounit_suite_t *test_suite;
gint result;

test_suite = au_new_suite(g_string_new(„Function Tests“), 0, 0);
au_add_test_group(test_suite, tests);
result = au_run_suite(test_suite);

au_delete_suite(test_suite);

return result;
}

Listing 3. Code für Funktions- Tests. Das Präfix _au verweist auf autounit. Die Bedingungen, die hier definiert sind, müssen alle erfüllt werden, ansonsten ist der Test nicht bestanden.

Wie die Bezeichnung Makefile.am schon suggeriert, ist es möglich, Test- Code mit der Bibliothek unter test zu binden. Mit von libtool erzeugten Bibliotheken wurde bis jetzt noch nicht entschieden, ob es sich hierbei um eine statische oder Shared Library handelt. Die entsprechenden Maßnahmen werden zum Zeitpunkt der Konfiguration durchgeführt. Library-Test-Code ist ähnlich zu normalen Funktionstests. Zusätzlich enthält der Code Stress-Tests für die Library, der im erfolgreichen Testfall laufen gelassen wird und prinzipiell nicht spezifisch ist für den Fall des Library-Tests.

Der Test von statischen Funktionen in normalem Code ist schwierig zu bewerkstelligen, weil Test-Code und Produktiv- Code nicht in Wechselwirkung zueinander treten sollen. Da es aber nicht gewünscht ist, den Produktiv- Code für Testzwecke abzuändern, muss hier einer von mehreren möglichen „Workarounds“ angewendet werden. Eine Möglichkeit ist die Einbeziehung der C-Quelldatei in den Test- Code durch

#include „../src/hello.c“

Diese Konstruktion ist nicht gerade eine allgemein vorgesehene Nutzung des C-Pre-Prozessors. Aber in diesem Fall funktioniert es sehr gut. Es muss nur darauf geachtet werden, dass die eingebundene C-Datei keine eigene main()-Funktion enthält und dass andere inkludierte Dateien mit dieser Konstruktion zusammenarbeiten.

Wie im Library-Testbeispiel oben gesehen, bietet die Bibliothek autounit eine gute Grundlage, um Stress-Tests anstelle normaler Tests durchzuführen. Die entsprechenden Tests werden genau n-mal durchgeführt, wobei n in au_run_stress_suite() spezifiziert wird.

Da der Quellcode nicht komplett aus C-Code besteht, können Testprogramme laufen gelassen werden, die als Shell-Skripte oder in anderen Sprachen vorliegen. Definiert im Makefile.am, müssen die entsprechenden Dateien nur in der Variable TESTS aufgelistet werden. Danach laufen sie automatisch mit den anderen Tests durch. Bei der Implementierung sollte aber darauf geachtet werden, dass das Programm auf dem Embedded-Target laufen soll. Das Beispiel-Paket enthält ein Skript namens test-test, welches running on ‘hostname’... in stdout schreibt.

Verbindung mit dem Target-System

Wie im Makefile.am angegeben, definiert die Variable TESTS_ENVIRONMENT eine SSH-Verbindung (verschlüsselte IP-Netzwerkverbindung) für die Verbindung zum Target-System. Um „make check“ davon abzuhalten, nach einem Passwort beim Login auf dem Target zu fragen, muss die „Public Key Authentication“ auf dem Target-System aktiviert werden. Eine detaillierte Schritt-für-Schritt- Beschreibung ist in der Datei Readme im Beispiel tarball enthalten. Wenn alles korrekt eingerichtet ist, sollte der Lauf von make check in etwa zu dem in Listing 4 gezeigten Resultat führen.

Interessant hierbei ist, dass der individuelle Output des Tests vom Target- System stammt, mit Ausnahme von PASS: Diese Zeilen sind auf dem Horst erzeugt worden. Die Statistiken („All 4 tests passed“) haben ihren Ursprung auf dem Entwicklungs-Host. Ereignet sich bei den Testdurchläufen ein Fehler, so ist der Output selbsterklärend – anstelle von „All 4 tests passed“ erscheint „x of 4 tests failed“ mit entsprechender Angabe bei den Einzeltests, welcher Schritt den Fehler verursachte.

[...]
make check-TESTS
make[2]: Entering directory `/home/stigge/elektronik/hellotest/tests’
Test running on targetdev...
PASS: test-test
Function Tests
.....
OK 5 succeeded 10.655778 s (655778 us) total elapsed time
PASS: test-functions
Static Tests
..
OK 2 succeeded 2.210588 s (210588 us) total elapsed time
PASS: test-static
Library Tests
.....
OK 5 succeeded 0.006428 s (6428 us) total elapsed time
Stress Tests (100 iterations)
(1).....(6).....(11).....(16).....(21).....(26).....(31).....(36).....(41).....(46).....(51).....(56).....(61).....(66).....(71).....(76).....(81).....(86).....(91).....(96).....
OK 5 succeeded 0.878457 s (878457 us) total elapsed time
PASS: test-library
==================
All 4 tests passed
==================
make[2]: Leaving directory `/home/stigge/elektronik/hellotest/tests’
[...]

Listing 4. Bildschirmausgabe bei erfolgreichem Durchlauf von make check.

Test-Code häufig umfangreicher als Produktiv-Code

Da jedes Embedded-Projekt sehr individuelle Randbedingungen aufweist, müssen die beschriebene Test-Umgebung und einige Ideen für jedes Projekt individuell modifiziert werden. Der Aufbau einer zweckmäßigen Testumgebung ist naturgemäß eine der schwierigsten Aufgaben beim Schreiben neuer Tests. Darüber hinaus sollte man nicht vergessen, dass Unit-Tests nicht an eine spezielle Implementierung eines Unit-Test-Frameworks wie autounit gebunden sind oder an eine besondere Sprache. Qualitätssicherung ist nicht allein damit erreicht, dass Unit-Tests in den Entwicklungsprozess eingebunden werden.

Das Wichtigste beim Unit-Testen ist: es zu tun! Zudem ist es sehr dringend angeraten, möglichst frühzeitig im Entwicklungsprozess damit anzufangen. Unit-Tests können eine drastische Verbesserung der Vorhersehbarkeit von Entwicklungsplänen bringen, weil sie die Zeit, die für das Debugging benötigt wird, massiv senken können. Dennoch sollte man auch immer den Umfang des erforderlichen Test- Codes berücksichtigen, der häufig deutlich größer ist als der zu testende Code. jk

Autor:

Roland Stigge
studierte Informatik an der Humboldt-Universität in Berlin und ist heute Software-Entwickler bei der Philosys Software GmbH in Unterschleißheim bei München. Er ist zugleich Debian-Entwickler und GNU-Maintainer und verfügt über umfangreiche Erfahrungen in weit verbreitet genutzten Open-Source-Betriebssystemen. Sein Hauptinteresse gilt der Embedded-Software-Entwicklung mit den Schwerpunkten Qualitätssicherung, Open- Source-Software und Linux-/Unix-Betriebssysteme.
roland.stigge@philosys.de

© 2007 WEKA Fachzeitschriften-Verlag GmbH
Alle Rechte vorbehalten

Verwandte Webseiten:
www.pc-magazin.de * www.pcgo.de * www.internet-magazin.de
www.franzis.de
AdTech Ad