Friday, April 16, 2010

Unit testing with Qt

It's very easy to do unit testing with Qt. If you have any previous experience with JUnit you shouldn't have any difficulties using QTestLib.

All that is required to create a unit test, besides adding 'QT += testlib' to your .pro file, is that the test class is a sub-class of QObject and each test function is declared as a private slot. To run all your test functions in a class simply call QTest::qExec() from the main function.

The following snippet shows what a unit test looks like:
#include <QtTest/QTest>

class MyTest : public QObject
{
Q_OBJECT
public:
    explicit MyTest(QObject *parent = 0) : QObject(parent) {}

private slots:
    /** Test function 1 */
    void test1()
    {
        QCOMPARE('a', 'a');
    }

    /** Test function 2 */
    void test2()
    {
        QVERIFY(10 != 11);
    }

};
int main(int argc, char *argv[])
{
    // Simple test cases doesn't require a
    // Q[Core]Application instance.
    MyTest t;
    QTest::qExec(&t);
}
// The next line is only required if we have the
// definition and implementation in the same file
// as in this case.
#include "qunittest.moc"
The test above produces the following output when executed:
Config: Using QTest library 4.6.2, Qt 4.6.2
PASS   : MyTest::initTestCase()
PASS   : MyTest::test1()
PASS   : MyTest::test2()
PASS   : MyTest::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
QVERIFY evaluates the expression and continues the execution if the expression evaluates to true. If the expression evaluates to false a message is appended to the test log and the test function is stopped. QCOMPARE is similar to QVERIFY and gives a more verbose output. In addition to the test functions the framework will look for the following four functions (must also be declared as private slots):
  • initTestCase() - Executed before any test function
  • cleanupTestCase() - Executed after the last test function
  • init() - Executed before each test function
  • cleanup() - Executed after each test function
Ok, now we know the basics, lets go on and check how to do data driven tests.

Data driven tests are used when you want to exercise some piece of code with different values and is supported by the Qt testlib. To feed test data to a test function add a function with the same name as the test function appended with a '_data' suffix. So if the test function is named testFeature, the function that serves the test with data should be named testFeature_data. The test data is setup in a table. Two functions are used to create this table, addColumn and newRow. addColumn is used to define elements in the table and newRow adds data. QFETCH is used to fetch data from the table.

Lets create a simple 'capitalize' function that takes a string, capitalizes the first character and returns the result as a new string. We want to test the function with different data such as an empty string, first character is lower case and first character is upper case.
#include <QtTest/QTest>

class MyTest : public QObject
{
Q_OBJECT
public:
    explicit MyTest(QObject *parent = 0) : QObject(parent) {}

    QString capitalize(const QString &text)
    {
        QString result = text;
        result[0] = text[0].toUpper();
        return result;
    }

private slots:
    void testCapitalize_data()
    {
        QTest::addColumn("string");
        QTest::addColumn("expected");

        QTest::newRow("empty string") << "" << "";
        QTest::newRow("lower case") << "lower case" << "Lower case";
        QTest::newRow("upper case") << "Upper case" << "Upper case";
    }

    void testCapitalize()
    {
        QFETCH(QString, string);
        QFETCH(QString, expected);
        QCOMPARE(capitalize(string), expected);
    }
};
// Macro that implements main
QTEST_APPLESS_MAIN(MyTest);
#include "qunittest.moc"
Running the test gives the following output:
********* Start testing of MyTest *********
Config: Using QTest library 4.6.2, Qt 4.6.2
PASS   : MyTest::initTestCase()
QFATAL : MyTest::testCapitalize(empty string) ASSERT: "i >= 0 && i < size()" in file ../../../../opt/qtsdk-2010.02/qt/include/QtCore/qstring.h, line 690
FAIL!  : MyTest::testCapitalize(empty string) Received a fatal error.
   Loc: [Unknown file(0)]
Totals: 1 passed, 1 failed, 0 skipped
********* Finished testing of MyTest *********
Oops, forgot to check if the string is empty. We fix that by adding the following check at the beginning of the capitalize function:
if (text.isEmpty()) {
    return text;
}
Running the test again:
********* Start testing of MyTest *********
Config: Using QTest library 4.6.2, Qt 4.6.2
PASS   : MyTest::initTestCase()
PASS   : MyTest::testCapitalize()
PASS   : MyTest::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of MyTest *********
Ok, this was a quick introduction to get you started. To find out more about the test lib read the manual, api docs, and the tutorial. You'll find examples on how to test your GUI by simulating mouse clicks, key presses and how to do simple benchmarking.