Suchen

Expertenbeitrag

 Mark Hermeling

Mark Hermeling

Senior Director Product Marketing, GrammaTech, Inc., GrammaTech, Inc.

Überblick und Anwendung von Mock-Funktionen Mock-Funktionen helfen bei Modultests von C-Programmen

Autor / Redakteur: Mark Hermeling / Matthias Brandstätter

Unabhängig davon, ob Sie auf das Credo des ‚Test Driven Development‘ setzen und das Testen an die erste Stelle setzen, gelten automatisierte Modultests doch generell als etwas, was der Softwarequalität dienlich ist.

Firmen zum Thema

Mock-Funktionen erleichtern Modultests bei C-Programmen.
Mock-Funktionen erleichtern Modultests bei C-Programmen.
(Bild: Pixabay / CC0 )

Weshalb sollte man auf Mock-Funktionen zurückgreifen? Unabhängig davon, ob Sie auf das Credo des ‚Test Driven Development‘ setzen und das Testen an die erste Stelle setzen, gelten automatisierte Modultests doch generell als etwas, was der Softwarequalität dienlich ist. Oftmals gibt es in Softwaresystemen bestimmte Teile, mit denen sich Modultests relativ einfach durchführen lassen. Meist handelt es sich dabei um wiederverwendbare Bibliotheken und andere Module in den Randzonen der Funktionalität eines Systems. Als schwierig kann sich dagegen das Testen der zentralen Funktionen eines Systems erweisen, also von Bestands-Code, bei dessen Entwicklung die Prüfbarkeit nicht berücksichtigt wurde, oder aber von solchem Code, der an das Verhalten der Hardware gebunden ist. Das Gliedern der Funktionalität in kleine, prüfbare Einheiten ist in diesen Situationen nicht immer einfach.

Wann nutze ich eine Mock-Funktion?

Einige dieser Probleme könnten durchaus per Refactoring, also durch eine Neugliederung des Codes gelöst werden. In der Praxis ist dies jedoch nicht immer möglich (man denke nur an Hindernisse wie die Change-Management-Prozesse, den Zeitaufwand für das Refactoring, die Risiken beim Refactoring von Code, für den noch keine Modultests durchgeführt wurden, usw.). Bei nicht objektorientierten Sprachen wie C können Modultests besondere Herausforderungen verursachen, da die Sprache keine Interface-Primitives mitbringt, mit denen sich einfach zwischen realem und testweisem Code wechseln lässt. Hier empfiehlt sich die Verwendung von Mock-Funktionen (Scheinfunktionen). Diese stellen in prozeduralen Sprachen das Gegenstück von Mock-Objekten dar („Endo-Testing: Unit Testing with Mock Objects", Mackinnon, et. al.) und können Hilfestellung beim Prüfen dieser schwierig erreichbaren Abschnitte Ihres Codes leisten.

In einer prozeduralen Sprache wie C bringt das Separieren einzelner Abschnitte zu Prüfzwecken gewisse Herausforderungen mit sich. Diese betreffen:

  • die Definition (und Pflege) globaler Variablen und Funktionen, damit ein Modul losgelöst vom umfangreicheren Softwaresystem kompiliert und gelinkt werden kann, und die
  • Isolation der zu testenden Programmteile vom Verhalten der Funktionen, die sie aufrufen.

Mit Mock-Funktionen lässt sich das Problem der Isolation eines zu testenden Programmmoduls lösen.

Was ist eine Mock-Funktion?

Eine Mock-Funktion ist eine extrem einfache Funktion, deren Verhalten durch den Modultest kontrolliert wird. Den einfachsten Ansatz, den ich je gesehen habe, bietet cmockery: Der Modultest gibt hier schlicht vor, was jede Prozedur während des Tests zurückliefert (für jeden einzelnen Aufruf). Der Testcode gibt also im Prinzip eine Liste von Werten vor, die die Mock-Funktion ausliest, wenn sie einen Rückgabewert benötigt.

Eine Mock-Funktion ist eine extrem einfache Funktion, deren Verhalten durch den Modultest kontrolliert wird.
Eine Mock-Funktion ist eine extrem einfache Funktion, deren Verhalten durch den Modultest kontrolliert wird.
(Bild: GrammaTech)

Hier könnte man jetzt eigens ein kleines Beispiel erstellen, aber besser ist ein Blick auf reale Software. Die nachfolgende Abbildung zeigt die von CodeSonar erzeugte Visualisierung des Open-Source-Shellprogramms bash (Version 4.1). Die Kanten geben hier Aufrufe der Funktionen untereinander wieder.

Die von CodeSonar erzeugte Visualisierung des Open-Source-Shellprogramms bash.
Die von CodeSonar erzeugte Visualisierung des Open-Source-Shellprogramms bash.
(Bild: GrammaTech)

Wie man sieht, gibt es in der Mitte einen großen Umfang an Kernfunktionalität innerhalb von bashline.c. Greifen wir hier einmal die Funktion command_subst_completion_function() heraus um zu demonstrieren, wie Mock-Funktionen bei Modultests helfen können. Die nächste Abbildung zeigt die CodeSonar-Visualisierung des Vorwärts-Aufrufdiagramms der von uns gewählten Funktion. Bei der Mehrzahl der direkt aufgerufenen Funktionen handelt es sich um API-Funktionen – mit Ausnahme von rt_completion_match() und test_for_directory(). Wir möchten command_subst_completion_function() vom Verhalten dieser anderen Funktionen isolieren, damit wir erstens die kleinstmögliche Einheit testen können und damit zweitens unser Modultest nicht mit den tatsächlichen Implementierungen dieser Funktionen verlinkt werden muss. Hierdurch nämlich könnten weitere Abhängigkeiten usw. hinzukommen, bis man sein komplettes Programm erstellt.

Das Bild zeigt die CodeSonar-Visualisierung des Vorwärts-Aufrufdiagramms der von uns gewählten Funktion.
Das Bild zeigt die CodeSonar-Visualisierung des Vorwärts-Aufrufdiagramms der von uns gewählten Funktion.
(Bild: GrammaTech)

Nun ist test_for_directory() eine File Static Funktion, die nicht durch eine Mock-Funktion ersetzt werden kann. Stattdessen werden für die beiden von ihr aufgerufenen Funktionen file_is_dir() und bash_tilde_expand() (beide befinden sich in anderen Dateien als unsere Funktion) Mock-Funktionen eingesetzt. Das nachfolgende Beispiel zeigt, wie die beiden Funktionen mit cmockery nachgebildet werden können:

Diese Abbildung zeigt, wie wie die beiden Funktionen mit cmockery nachgebildet werden können.
Diese Abbildung zeigt, wie wie die beiden Funktionen mit cmockery nachgebildet werden können.
(Bild: GrammaTech)

Der einfachste Test für unsere Funktion könnte darin bestehen, keine Command Completion Matches, keine Tilde Expansions und keine Verzeichnisse zu haben. Ohne auch nur eines der Details im bash-Code zu verstehen, würden wir erwarten, dass ein Test ungefähr so aussieht:

Ein einfacher Test für unsere Funktion könnte so aussehen.
Ein einfacher Test für unsere Funktion könnte so aussehen.
(Bild: GrammaTech)

Damit haben wir also das Verhalten der Funktionen, von denen wir abhängig sind, auf möglichst einfache Weise nachgebildet. Um eine gute Testüberdeckung von command_subst_completion_function() zu erreichen, würden wir einfach weiter Testfunktionen generieren, die unterschiedliche Scheinwerte zurückgeben.

Zusammenfassung

Es gibt eine große Zahl weiterer Modultest-Frameworks, die die Verwendung von Mock-Funktionen unterstützen (dies gilt beispielsweise für viele aus diesem umfangreichen Wikipedia-Artikel). Es ist aber zu hoffen, dass die gezeigten einfachen Beispiele mit cmockery bereits deutlich machen konnten, weshalb Mock-Funktionen für das Testen realer Programme nützlich sind.

Das Testen all dieser Einzelheiten bricht die Komplexität des beobachteten Verhaltens so weit herunter, dass es einfacher wird, eine gute Testüberdeckung zu erreichen (die Beziehungen zwischen Eingaben und Ausgaben sind auf der Modulebene bedeutend unkomplizierter) und die Ursache von Fehlern aufzuspüren (die Stecknadeln sind in einem kleineren Heuhaufen – in einem Modul – einfacher zu finden).

An dieser Stelle sei eine der Schlussfolgerungen aus A Survey of Unit Testing Practices zitiert: „Es ist schwierig eine relevante Prüfumgebung für Module zu erstellen, die mit einem komplexen Systemstatus oder einer komplexen Systemumgebung interagieren.“ Mock-basierte Modultests bieten eine Möglichkeit diese Schwierigkeiten zu entschärfen, indem sie die Programmfunktionalität isolieren. Dabei müssen allerdings die Abhängigkeiten von den Kompilier- und Linkzeiten in Kauf genommen werden, die sich bei einem typischen C-Programm ergeben.

(ID:44414537)

Über den Autor

 Mark Hermeling

Mark Hermeling

Senior Director Product Marketing, GrammaTech, Inc., GrammaTech, Inc.