End to End GUI Development with Qt5
上QQ阅读APP看书,第一时间看更新

Custom approach

The custom approach we will take still applies the same basic concepts we've just discussed.  At the heart of it, we will still have a test class that contains a suite of unit test methods to be executed. All we will do is supplement this with some additional boilerplate code to allow us to easily accommodate multiple test classes and pipe the output to files rather than the console.

Let’s begin by adding a new class TestSuite to cm-tests in the source folder:

test-suite.h:

#ifndef TESTSUITE_H
#define TESTSUITE_H
#include <QObject> #include <QString> #include <QtTest/QtTest>
#include <vector>
namespace cm {
class TestSuite : public QObject { Q_OBJECT public: explicit TestSuite(const QString& _testName = ""); virtual ~TestSuite();
QString testName; static std::vector<TestSuite*>& testList(); };
}
#endif

test-suite.cpp:

#include "test-suite.h"
#include <QDebug>
namespace cm {
TestSuite::TestSuite(const QString& _testName) : QObject() , testName(_testName) { qDebug() << "Creating test" << testName; testList().push_back(this); qDebug() << testList().size() << " tests recorded"; }
TestSuite::~TestSuite() { qDebug() << "Destroying test"; }
std::vector<TestSuite*>& TestSuite::testList() { static std::vector<TestSuite*> instance = std::vector<TestSuite*>(); return instance; }
}

Here, we are creating a base class that will be used for each of our test classes. There is generally a one-to-one relationship between a regular class and a test suite class, for example, the Client and ClientTests classes. Each derived instance of TestSuite adds itself to a shared vector. This can be a little confusing at first glance, so we are also writing some information out to the console using qDebug() so that you can follow what’s going on. It will make more sense when we create our first class deriving from TestSuite.

Next, add a new C++ Source File main.cpp, again to the source folder:

main.cpp:

#include <QtTest/QtTest>
#include <QDebug>
#include "test-suite.h"
using namespace cm;
int main(int argc, char *argv[]) { Q_UNUSED(argc); Q_UNUSED(argv);
qDebug() << "Starting test suite..."; qDebug() << "Accessing tests from " << &TestSuite::testList(); qDebug() << TestSuite::testList().size() << " tests detected";
int failedTestsCount = 0;
for(TestSuite* i : TestSuite::testList()) { qDebug() << "Executing test " << i->testName; QString filename(i->testName + ".xml"); int result = QTest::qExec(i, QStringList() << " " << "-o" <<
filename << "-xunitxml");
qDebug() << "Test result " << result; if(result != 0) { failedTestsCount++; } }
qDebug() << "Test suite complete - " <<
QString::number(failedTestsCount) << " failures detected.";
return failedTestsCount; }

This looks more complicated than it actually is because of the qDebug() statements added for information. We iterate through each of the registered test classes and use the static QTest::qExec() method to detect and run all tests discovered within them. A key addition, however, is that we create an XML file for each class and pipe out the results to it.

This mechanism solves our two problems. We now have a single main() method that will detect and run all of our tests, and we get a separate XML file containing output for each of our test suites. However, before you can build the project, you will need to revisit client-tests.cpp and either comment out or remove the QTEST_APPLESS_MAIN line, or we'll be back to the problem of multiple main() methods. Don’t worry about the rest of client-tests.cpp for now; we’ll revisit it later when we start testing our data classes.

Build and run now, and you’ll get a different set of text in Application Output:

Starting test suite...
Accessing tests from 0x40b040
0 tests detected
Test suite complete - "0" failures detected.

Let’s go ahead and implement our first TestSuite. We have a MasterController class that presents a message string to the UI, so let's write a simple test that verifies that the message is correct. We will need to reference code from cm-lib in the cm-tests project, so ensure that the relevant INCLUDE directives are added to cm-tests.pro:

INCLUDEPATH += source 
 ../cm-lib/source

Create a new companion test class called MasterControllerTests in cm-tests/source/controllers.

master-controller-tests.h:

#ifndef MASTERCONTROLLERTESTS_H
#define MASTERCONTROLLERTESTS_H
#include <QtTest>
#include <controllers/master-controller.h> #include <test-suite.h>
namespace cm { namespace controllers {
class MasterControllerTests : public TestSuite { Q_OBJECT
public: MasterControllerTests();
private slots: /// @brief Called before the first test function is executed void initTestCase(); /// @brief Called after the last test function was executed. void cleanupTestCase(); /// @brief Called before each test function is executed. void init(); /// @brief Called after every test function. void cleanup();
private slots: void welcomeMessage_returnsCorrectMessage();
private: MasterController masterController; };
}}
#endif

We’ve explicitly added the initTestCase() and cleanupTestCase() scaffolding methods so that there is no mystery as to where they come from. We've also added another couple of special scaffolding methods for completeness: init() and cleanup(). The difference is that these methods are executed before and after each individual test, as opposed to before and after the entire suite of tests.

None of these methods are doing anything for us and are there just for future reference. They can safely be removed if you want to streamline things.

master-controller-tests.cpp:

#include "master-controller-tests.h"

namespace cm {
namespace controllers { // Instance
static MasterControllerTests instance;
MasterControllerTests::MasterControllerTests() : TestSuite( "MasterControllerTests" ) { }
}
namespace controllers { // Scaffolding
void MasterControllerTests::initTestCase() { }
void MasterControllerTests::cleanupTestCase() { }
void MasterControllerTests::init() { }
void MasterControllerTests::cleanup() { }
}
namespace controllers { // Tests
void MasterControllerTests::welcomeMessage_returnsCorrectMessage() { QCOMPARE( masterController.welcomeMessage(), QString("Welcome to the Client Management system!") ); }
}}

We again have a single test, but this time, it actually serves some meaningful purpose. We want to test that when we instantiate a MasterController object and access its welcomeMessage method, it returns the message that we want, which will be Welcome to the Client Management system!.

Unlike the scaffolding methods, the naming of your tests is entirely down to preference. I tend to loosely follow the methodIAmTesting_givenSomeScenario_doesTheCorrectThing  format, for example:

divideTwoNumbers_givenTwoValidNumbers_returnsCorrectResult()
divideTwoNumbers_givenZeroDivisor_throwsInvalidArgumentException()

We construct an instance of MasterController as a private member variable that we will use to test against. In the implementation, we specify the name of the test suite via the constructor, and we also create a static instance of the test class. This is the trigger that adds MasterControllerTests to the static vector we saw in the TestSuite class.

Finally, for the implementation of our test, we test the value of the welcomeMessage of our masterController instance with the message we want using the QCOMPARE macro. Note that because QCOMPARE is a macro, you won’t get implicit typecasting, so you need to ensure that the types of the expected and actual results are the same. Here, we’ve achieved that by constructing a QString object from the literal text.

Run qmake, and build and run to see the results of our test in the Application Output pane:

Creating test "MasterControllerTests"
1 tests recorded
Starting test suite...
Accessing tests from 0x40b040
1 tests detected
Executing test "MasterControllerTests"
Test result 1
Test suite complete - "1" failures detected.
Destroying test

This begins with the registration of the MasterControllerTests class via the static instance. The main() method iterates the collection of registered test suites and finds one, then executes all the unit tests within that suite. The test suite contains one unit test that runs and promptly fails. This may seem to be less helpful than earlier as there is no indication as to which test failed or why. However, remember that this output is simply from the qDebug() statements we added for extra information; it is not the true output from the test execution.  In master-controller-tests.cpp we instantiated the TestSuite with a testName parameter of MasterControllerTests, so the output will have been piped to a file named MasterControllerTests.xml.

Navigate to the cm/binaries folder and drill down through the folders to where we direct our project output for the selected configuration and in there, you will see MasterControllerTests.xml:

<testsuite name="cm::controllers::MasterControllerTests" tests="3" failures="1" errors="0">
    <properties>
       <property name="QTestVersion" value="5.10.0"/>
       <property name="QtVersion" value="5.10.0"/>
       <property name="QtBuild" value="Qt 5.10.0 (i386-little_endian- 
ilp32 shared (dynamic) debug build; by GCC 5.3.0)"/> </properties> <testcase name="initTestCase" result="pass"/> <testcase name="welcomeMessage_returnsCorrectMessage"
result="fail"> <failure result="fail" message="Compared values are not the same Actual (masterController.welcomeMessage) : "This is MasterController to Major Tom" Expected (QString("Welcome to the Client Management system!")): "Welcome to the Client Management system!""/> </testcase> <testcase name="cleanupTestCase" result="pass"/> <system-err/> </testsuite>

Here, we have the full output from the tests, and you can see that the failure was because the welcome message we got from masterController was This is MasterController to Major Tom, and we expected Welcome to the Client Management system!.

MasterController is not behaving as expected, and we’ve found a bug, so head over to master-controller.cpp and fix the problem:

QString welcomeMessage = "Welcome to the Client Management system!";

Rebuild both projects, execute the tests again, and bask in the glory of a 100% pass rate:

Creating test "MasterControllerTests"
1 tests recorded
Starting test suite...
Accessing tests from 0x40b040
1 tests detected
Executing test "MasterControllerTests"
Test result 0
Test suite complete - "0" failures detected.
Destroying test

Now that we have the testing framework set up, let’s test something a little more complex than a simple string message and validate the work we did in the last chapter.