Liebe Leserinnen, Liebe Leser,
heute möchte ich eine Lösung eines Problems teilen, auf das ich während der Nutzung des QtTest-Frameworks getroffen habe. Das Problem geht um die Ausführung von mehrere Testklassen durch eine einzige Testanwendung. Als ich dieses Thema erforscht habe, habe ich bemerkt, dass QtTest nie beabsichtigt wurde, die Tests aus einem zentralen Ort ausführen zu können. Stattdessen empfiehlt die Qt Dokumentation, ein eigenes Anwendung für jede Testklasse zu haben. Hier ist ein Beispiel:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// myguiclasstest.cpp
#include "MyGuiClass.h"
#include <QtTest>
class MyGuiClassTest : public QObject {
Q_OBJECT
private slots:
void myClassShouldDoCoolStuff();
};
MyGuiClassTest::myClassShouldDoCoolStuff()
{
// ...
}
QTEST_MAIN(MyGuiClassTest)
#include "myguiclasstest.moc"
Der oben zu sehende Code beruht auf die Empfehlungen der Qt Dokumentation, und solange man nur eine einzige zu prüfende Klasse hat, funktioniert das befriedigend. Aber würde passieren, wenn man auch eine andere Klasse testen will, und deswegen ersetzt man alles zwischen #include
Natürlich, das wird nicht funktionieren, also man sollte dieses Makro irgendwie umgehen. Es wäre auch gut, wenn der #include mit der moc-Datei entsorgt werden könnte. Dieser #include wird nur wegen technischen Gründen benötigt, weil die Testklasse in einer Quellcodedatei statt der gewöhnlichen Header+Quellcodedatei deklariert und definiert ist. Eine schnelle und naive Lösung für dieses Problem ist die Umstellung der Datei zu einer Headerdatei, aber weil die main() Funktion in einer Headerdatei nicht sein kann, wird das nicht funktionieren. Was wäre dann eine nette und elegante Lösung?
Die Nutzung einer Ausführungsklasse könnte ein guter Ansatz sein, aber meiner Meinung nach sind solche Ansätze ein bisschen zu komplex für solche einfachen Aufgaben, wie die Ausführung von mehrere Testfunktionen. Stattdessen habe ich mich eine einfachere Lösung ausgedacht, die aus der Kopierung von den entsprechenden Sachen aus QTEST_MAIN zu einer Templatefunktion steht:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// testrunner.h
#ifndef TESTRUNNER_H
#define TESTRUNNER_H
#include <QtTest>
template <typename TestClass>
void runTests(int argc, char* argv[], int* status)
{
QApplication app(argc, argv);
app.setAttribute(Qt::AA_Use96Dpi, true);
QTEST_DISABLE_KEYPAD_NAVIGATION TestClass tc;
*status |= QTest::qExec(&tc, argc, argv);
}
#endif // TESTRUNNER_H
Im Vergleich zu den anderen Lösungen ist es eine einfache Methode, und die Nutzung ist gleichermaßen einfach.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// main.cpp
#include "myguiclasstest.h"
#include "mydialogclasstest.h"
#include "testrunner.h"
int main(int argc, char *argv[])
{
int status = 0;
runTests<MyGuiClassTest>(argc, argv, &status);
runTests<MyDialogClassTest>(argc, argv, &status);
return status;
}
Seitdem der Ausführungscode aus dem Makro ausgeschnitten wurde, kann QTEST_MAIN aus der Testdatei ohne Angst entfernt werden. Das ist auch bemerkenswert, dass die Testklassen nun in einer Header-Datei statt eine Quellcodedatei sind. Das bedeutet, dass sie jetzt in main.cpp normal eingefügt werden können, und der obengenannte moc-bezogene #include können auch entfernt werden. Letztendlich kann der Code der Testfunktionen mit inline markiert werden. Hier ist der modifizierte Code:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// myguiclasstest.h
#ifndef MYGUICLASSTEST_H
#define MYGUICLASSTEST_H
#include "MyGuiClass.h"
#include <QtTest>
class MyGuiClassTest : public QObject {
Q_OBJECT
private slots:
void myClassShouldDoCoolStuff()
{
// ...
}
};
#endif // MYGUICLASSTEST_H
Es gibt sowohl gute, als auch schlechte Nachrichten. Die gute Nachricht ist, dass alles wie erwartet funktioniert, alle Tests können ausgeführt werden, Qt Creator kann die Ergebnisse fassen, und die Testergebnisse erscheinen in der “Test Results” Registerkarte. Die schlechte Nachricht ist, dass in dem Seitenfenster für Testauswahl nur die letzte geschriebene Klasse erscheinen wird. Das bedeutet, dass man diese Tests durch dieses Fenster nicht ein/ausschalten kann. Es gibt dennoch eine andere wichtige Frage: wird das auf diese Weise eingestellte QtTest Framework mit anderen Frameworks (z.B. Google Test) gleichzeitig funktionieren? Warum würde man dennoch das brauchen? Einfach gesagt, weil ich Google Test für eine bessere Alternative für nicht GUI-bezogene Tests halte, also ich möchte ein Projekt erstellen, in dem beide Frameworks für verschiedene Teile des Projekts gleichzeitig verwendet werden können.
Nehmen wir an, dass ich QtTest für alle GUI-bezogene Sachen und Google Test für alle andere Sachen verwenden möchte. Dafür braucht man zwei getrennte Projekte (weil beide Frameworks brauchen eine eigene Anwendung). Hier gibt’s einen Beispielprojektbaum:
MyCoolProject
|--MyCoolProj.pro
|--MyCoolApp
|--MyCoolApp.pro
\--main.cpp
|--MyGuiLibs
|--MyGuiLibs.pro
|--MyGuiClassLib
|--MyGuiClassLib.pro
|--mylib1decl.h
|--mylib1.h
\--mylib1.cpp
\--MyDialogClassLib
|--MyDialogClassLib.pro
|--mylib2decl.h
|--mylib2.h
\--mylib2.cpp
|--MyLibs
\--MyCoolLib
|--MyCoolLib.pro
|--mylib3.h
\--mylib3.cpp
|--GoogleTest
|--googletest
|--ci
|--googlemock
|--googletest
|-- ...
\--WORKSPACE
|--GoogleTest.pri
\--GoogleTest.pro
\--Tests
|--Tests.pro
|--GuiTests
|--GuiTests.pro
|--main.cpp
|--myguiclasstest.h
\--mydialogclasstest.h
|--NonGuiTests
|--NonGuiTests.pro
|--main.cpp
\--tst_mycoolstuff.cpp
Glücklicherweise ist das mithilfe qmake (beziehungsweise CMake) ganz möglich zu erreichen.
Zuallererst braucht man ein Subdirs-Projekt für alle Testprojekte:
qmake
1
2
3
4
TEMPLATE = subdirs
SUBDIRS += NonGuiTests \
GuiTests
Dann ist es nötig, die eigene Projektdateien für die Testprojekte zu erstellen:
qmake
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// GuiTests.pro
QT += testlib
QT += gui widgets
CONFIG += testcase
CONFIG += qt warn_on depend_includepath
CONFIG += c++14
CONFIG += cmdline
TEMPLATE = app
ROOTPATH = ../..
DEPENDPATH += $$ROOTPATH/MyGuiLibs/MyGuiClassLib \
$$ROOTPATH/MyGuiLibs/MyDialogClassLib
INCLUDEPATH += $$ROOTPATH/MyGuiLibs/MyGuiClassLib \
$$ROOTPATH/MyGuiLibs/MyDialogClassLib
LIBS += -L$$ROOTPATH/MyGuiLibs/MyGuiClassLib -lMyGuiClassLib \
-L$$ROOTPATH/MyGuiLibs/MyDialogClassLib -lMyDialogClassLib
SOURCES += \
main.cpp
HEADERS += \
myguiclasstest.h \
mydialogclasstest.h \
testrunner.h \
qmake
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// NonGuiTests.pro
TARGET = NonGuiTests
TEMPLATE = app
CONFIG += c++14
CONFIG += testcase #Creates 'check' target in Makefile.
CONFIG -= debug_and_release
CONFIG += cmdline
ROOTPATH = ../..
INCLUDEPATH += $$ROOTPATH/MyLibs/MyCoolStuffLib
LIBS += -L$$ROOTPATH/MyLibs/MyCoolStuffLib -lMyCoolStuffLib
include($$ROOTPATH/GoogleTest/GoogleTest.pri)
HEADERS += \
SOURCES += main.cpp \
tst_mycoolstuff.cpp \
Dieser Ansatz ermöglicht alles nach Bedarf und ohne Aufwand auszuführen. Mein einziges Problem ist das obengenannte Fehlen von Ausschaltsmöglichkeit der Tests in Creator, aber die Behebung dieses Problems wird für später verschoben.
Wie immer, vielen Dank fürs Lesen.
UPDATE:
Nachdem ich diese Lösung mit Creator ein bisschen benutzt habe, wurde jedoch deutlich, dass Creators Erkennung von Tests ist seltsam, weil nach das Schließen und Öffnung von Creator keine Tests erkannt wurden. Als es sich herausstellte, die Liste von Tests, die während einer Session erstellt wurde, wird nicht zu den späteren Sessionen gespeichert. Stattdessen wird diese Liste bei jeder Öffnung des Projekts erneut erstellt. Dieses Verhalten würde zu kein Problem führen, aber wenn eine Liste von Grund auf oder schrittweise während der Session erstellt wird, ergibt es das gleiche Ergebnis nicht.
Ich versuchte dieses Problem durch den Erstaz von der obengenannten Template-Funktion durch ein Funktion-Makro umzugehen:
qmake
1
2
3
4
5
6
7
# define RUN_TESTS(test_class, argc, argv, status) \
{ \
QApplication app(argc, argv); \
app.setAttribute(Qt::AA_Use96Dpi, true); \
QTEST_DISABLE_KEYPAD_NAVIGATION test_class tc; \
*status |= QTest::qExec(&tc, argc, argv); \
} \
Weil Makros in der Kompilierungsvorgang ein Schritt früher als Templates verarbeitet werden, have ich gedacht, dass Creator die Tests auf diese Weise besser erkennen wird. Leider hatte ich Unrecht. Nachdem Creator mal wieder geschlossen und dann geöffnet wurde, waren alle Tests erneut verschwunden. Das größte Problem ist, dass sofern mindenstens ein Test nicht erkennt wurde, werden keine Tests ausgeführt. Deswegen sollte man mindenstens ein Makro zu der main()-Funktion erweitern, um alle Tests ausführen zu können. An dieser Stelle spielt es keine Rolle, ob man ein Makro oder eine Template-Funktion für den Rest der Tests verwendet, weil beide funktionierbar zu sein scheinen. Das ist doch eine sehr hässliche Umgehungslösung, aber jetzt kenne ich keine bessere Methode für die Ausführung der Tests. Weiterhin, ich hatte keine Zeit den Erkennungsvorgang von Creator zu untersuchen. Das soll jetzt reichen…