Google Mock
Wie der Name bereits vermuten lässt, ist Google Mock ein Mock-Framework. Mit Hilfe des Frameworks
kann leicht überprüft werden, ob Funktionen wie erwartet aufgerufen und definiert
werden, was eine noch zu implementierende Funktion zurückgeben soll. Integration und Build funktionieren
analog
zu dem bereits vorgestellten Framework Google Test.
Hierbei sind verschiedene Arten von Tests zu unterscheiden. Als Beispiele seien
Unit-Tests zum Testen einer Komponente oder Integration-Tests, die das Zusammenspiel
mehrerer Komponenten prüfen, genannt.
Für die meisten Programmiersprachen gibt es zudem Frameworks, die das Erstellen und
Ausführen bedeutend erleichtern, so auch für C++.
Allgemeines
Die Verwendung von Google Mock ist neben Google Test auch mit anderen Test-Frameworks möglich,
aufwendiger ist dann jedoch die bereits erwähnte Integration. Daher beziehen sich die
nachfolgenden Ausführungen auf die gemeinsame Verwendung mit Google Test.
Zunächst muss hierfür der Header ergänzt werden. Zudem ist es sinnvoll, den Namespace
„testing“ sichtbar zu machen:
# include <gtest/gtest.h>
# include <gmock/gmock.h>
using namespace testing ;
Um zu verstehen, wie Google Mock funktioniert, ist es notwendig sich zunächst mit der
Begrifflichkeit Mock auseinanderzusetzen. Ein Mock Objekt implementiert dasselbe
Interface wie das reale Objekt. Der wesentliche Unterschied ist
jedoch, dass ein Mock-Objekt zur Laufzeit Informationen darüber erhält, welche Aufrufe
auf es erwartet werden und wie es reagieren soll.
Erstellung eines Mock Objektes
Zum Mocken von Funktionen werden die Makros MOCK_METHOD benötigt. Wie diese umzusetzen
sind, wird nachfolgend an einem Beispiel erklärt.
Als Beispiel dient eine Applikation, die eine Liste von Usern anzeigen soll. Diese ist
nach dem verbreiteten Model View Controller (MVC) Konzept erstellt worden und enthält
einen UserListController. Dieser Controller ist die Funktion, die getestet werden soll.
Die UI, die auf diesen Controller gesetzt wird, soll auf gewisse Situationen reagieren.
Sobald das Laden der User abgeschlossen ist, muss die UI aktualisiert werden. Falls ein
User gelöscht wurde, muss dieser aus der Liste entfernt werden. Über den Contoller, kann
hierfür ein Listener registriert werden. Der Controller benötigt außerdem Informationen
wie den aktuellen Account, für den er die Liste der User laden soll. Hierfür wird dem
Controller ein DataProvider injiziert.
Die beiden Interfaces sollen wie folgt definiert sein:
UserListControllerListener
class UserListControllerListener {
public :
virtual void onRefreshData () = 0;
virtual void onRemoveItem (int position ) = 0;
};
UserListControllerDataProvider
class UserListControllerDataProvider {
private :
virtual long getAccountId () = 0;
};
Nun werden die beiden Mocks definiert, die es ermöglichen sollen, das Verhalten der
UserListController Schnittstellen zu testen. Dazu wird eine neue Klasse angelegt, welche
die Mock-Implementierung beinhaltet.
UserListControllerListenerMock
class UserListControllerListenerMock : public UserListControllerListener
{
public :
MOCK_METHOD0 ( onRefreshData , void ());
MOCK_METHOD1 ( onRemoveItem , void (int));
};
UserListControllerDataProviderMock
class UserListControllerDataProviderMock : public
UserListControllerDataProvider {
public :
MOCK_METHOD0 ( getAccountId , long ());
};
Wie zu erkennen ist, wird der Mock im Header implementiert, weiterer Code in einer Cpp
ist hierfür nicht notwendig. Andere Dinge, die weniger ersichtlich sind, müssen jedoch
auch berücksichtigt werden. Die Mock-Implementierung erbt von der zu mockenden Klasse
UserListControllerDataProviderMock
: public UserListControllerDataProvider. Die Vererbung muss public sein, ansonsten kann
es zu Kompilierfehlern kommen. Das Gleiche gilt auch auf die Sichtbarkeit innerhalb der Klasse.
Im UserListControllerDataProvider ist die Funktion getAccountId als private markiert, im
zugehörigen Mock jedoch als public. Dieser Umstand ist in C++ jedoch unproblematisch, da
beim Überschreiben einer Funktion deren Sichtbarkeit verändert werden kann. Es ist also
möglich, Funktionen im Code mit beliebiger Sichtbarkeit zu definieren. Es muss lediglich
bei der Implementierung des Mocks darauf geachtet werden, diese dort als public zu
markieren.
Die Makros selbst haben eine Zahl am Ende z.B. MOCK_METHOD0. Die Zahl entspricht der
Anzahl, der Parameter, die die zu mockende Funktion besitzt. Auch hier erhält man eine
kryptische Fehlermeldung beim Kompilieren, sollten die Anzahlen nicht
übereinstimmen.
Es sei außerdem erwähnt, dass es für die Implementierung des Mock nicht von Bedeutung
ist, ob es sich um rein virtuelle Funktionen handelt oder nicht. Allerdings ändert sich
für nicht virtuelle Funktionen die Implementierung geringfügig.
Wer sich Zeit sparen und die Mock-Klassen nicht von Hand schreiben möchte, kann ein von
Google bereitgestelltes Python Skript verwenden, das von einem Header die
Mock-Implementierung generieren kann. Gerade bei kleineren Interfaces sollte jedoch der
zeitliche Aufwand abgewogen werden.
Expectations
Für Tests, die Mocks verwenden, muss festgelegt werden, was von den jeweiligen Mocks erwartet wird.
Wesentlich hierbei ist jedoch, die richtigen Erwartungen zu haben. Sind diese zu strikt,
schlagen die Tests möglicherweise auch bei Änderungen fehl, die nichts mit dem
jeweiligen Test zu tun haben. Werden jedoch zu lockere Erwartungen gestellt, können
mögliche Fehler im Code eventuell übersehen werden.
Für die Erstellung von Expectations stehen verschiedene Konfigurationsmöglichkeiten zur
Verfügung, die an einem Mock vorgenommen werden können:
- Cardinalities
Wie oft soll die Funktion aufgerufen werden?
- Matchers
Welche Argumente werden erwartet?
- Actions
Wie soll die Mock-Implementierung auf den Aufruf reagieren?
Die Begriffe Matchers, Cardinalities und Actions werden von Google vorgegeben.
Expectations mit Cardinalities
Betrachtet werden soll eine einfache Expectation anhand des oben beschriebenen Beispiels.
Getestet wird, ob das onRefreshData aufgerufen wird. Dabei wird davon ausgegangen, dass
die Instanz des UserListControllerListenerMock bereits erzeugt und auf dem
UserListController als Listener registriert wurde. Die Instanz des Mock Objektes ist der
mockListener. Die definierte Expectation ist wie folgt definiert:
EXPECT_CALL ( mockListener , onRefreshData ()). Times (1);
mUserListControler -> loadData ();
Dem Mock-Listener wird mitgegeben, dass die Funktion onRefreshData genau einmal
aufgerufen werden soll. Wird sie hingegen zweimal oder überhaupt nicht aufgerufen,
schlägt der Test fehl. Zunächst muss das Mock konfiguriert werden, bevor der Aufruf
ausgeführt wird, welcher das Mock-Objekt verwendet.
Die Kardinalität kann als direkte Zahl angegeben werden. In diesem Fall wird erwartet,
dass die Funktion genau einmal aufgerufen wird. Auch Null ist eine korrekte Angabe.
Damit wird überprüft, dass die Funktion nicht aufgerufen wird. Es ist außerdem möglich,
eine Mindestanzahl anzugeben:
EXPECT_CALL ( mockListener , onRefreshData ()). Times ( AtLeast (1));
Im obigen Beispiel muss die Funktion mindestens einmal aufgerufen werden, damit der Test
erfüllt ist. Er ist jedoch auch erfolgreich, wenn sie mehr als einmal aufgerufen
wird.
Expectations mit Matchers
Im nachfolgenden Beispiel werden die Argumente des Aufrufs überprüft:
EXPECT_CALL ( mockListener , onRemoveItem (Eq (5)). Times (1) ;
Es soll getestet werden, ob die Funktion onRemoveItem mit dem Argument 5 aufgerufen wird.
Eq ist ein Matcher, der bereits im Abschnitt Assertion mit Matchern (Teil 1 des
Beitrags) auftaucht. Es ist also möglich eine ganze Reihe verschiedener Überprüfungen
vorzunehmen. Es stehen Matcher für viele mathematische Vergleiche (z.B. Ge() ist ≥) zur
Verfügung. Die Existenz von Objekten kann mit den Matchern IsNull() und IsNotNull()
geprüft werden. Der Parameter „_“ steht für die Wildcard Any. Wird er anstelle von Eq(5)
übergeben, ist der Aufruf mit jedem Parameter gültig.
Expectations mit Actions
Mit Actions können einem Mock bestimmte Verhalten übergeben werden. Ein einfaches
Beispiel hierfür ist die Definition eines Rückgabewertes.
EXPECT_CALL ( mockDataProvider , getAccountId (). WillOnce ( Return (17) );
Es wird während des Aufrufs der Funktion der Wert 17 zurückgegeben. Alle Funktionen sind
in einem EXPECT_CALL verwendbar. Es ist auch keine explizite Kardinalität vorgegeben.
Google Mock kann anhand der Aufrufe von WillOnce die Kardinalität errechnen.
Mehrere Expectations in einem Test
Es kann durchaus vorkommen, dass mehr als eine Expectation in einem Integration-Test
enthalten ist. Werden mehrere Expectations innerhalb einer Funktion angewandt, ist zudem
die Reihenfolge der Verarbeitung bzw. Auswertung relevant. Expectations werden in der
umgekehrten Reihenfolge ihrer Definition ausgewertet. Für die Action und Kardinalität
wird immer die erste Expectation herangezogen, deren Matcher passt.
Beispiel für die Auswertung von Expectations
Mehrere Expectations
EXPECT_CALL ( mockListener , onRemoveItem (_)). Times ( AtLeast (1));
EXPECT_CALL ( mockListener , onRemoveItem (Eq (42) )). Times (3);
Es sind mehrere Szenarien denkbar, die in verschiedenen Ergebnissen resultieren. Wird die
Funktion onRemoveItem viermal mit dem Wert 42 aufgerufen, so schlägt dieser Test fehl.
Bei allen Aufrufen mit dem Wert 42 greift die untere Expectation. Im Test wird über
Times (3) vorgegeben, dass der Aufruf mit 42 dreimal erwartet wird. Wenn die Funktion
jedoch dreimal mit dem Wert 42 aufgerufen wird, ohne dass ein weiterer Aufruf erfolgt,
so schlägt der Test fehl. Denn in der ersten Zeile ist definiert, dass die Funktion
mindestens einmal mit einem beliebigen anderen Wert aufgerufen werden soll. So ist der
Test erfolgreich, wenn zunächst dreimal der Wert 42 aufgerufen wird und anschließend ein
weiterer mit 75 erfolgt.
Zusammenfassung
Die Ausführungen zeigen auf, dass Google Test und Google Mock mächtige Werkzeuge für das Schreiben
von automatisierten Tests in C++ sind. Die Bibliotheken können unter der Verwendung von
CMake einfach in bestehende Projekte integriert werden.
Google Test bietet eine Vielzahl einfacher Möglichkeiten, Assertions zu schreiben. Tests können
durch Log-Nachrichten und Scoped Traces mit weiteren Informationen ausgestattet werden.
Zudem bietet das Framework die Möglichkeit, komplexere Themen wie Exceptions und Aborts
zu prüfen.
Die größte Limitierung der Frameworks stellt hingegen der fehlende Support für Tests von
asynchronen Funktionalitäten dar. Auch dass Google Test unter manchen Plattformen nicht als
Threadsave gilt, könnte Anwender dieser Plattformen vor Herausforderungen stellen.