Thursday, October 8, 2009

Interesting testing

QTestLib is a framework provided by Nokia to create unit tests for applications developed using Qt.

It's a lightweight library, with several features like test initialization and cleanup, benchmarking and data driven testing. One thing it provides I found very useful it's the ability to simulate GUI events, both clicking a sequence of keys or generating mouse events (clicking, double clicking, moving) .

As a proof of concept, I modified the calculator example provided by Qt (in folder $QTDIR/examples/widgets/calculator) adding a public method Calculator::result() in order for any other widget creating a Calculator object to retrieve the current result, and also adding a Calculator::keyPressEvent() method to handle the press of certain keys (see Listing 1).

With those changes, the resulting calculator can be used with the keyboard for the basic operations (selecting numbers, plus, minus, multiply, divide, clear all and backspace).

So next step was to write a basic test (see Listing 2) for the calculator with help from the QTestLib. This test takes advantage of the capability to store a series of GUI events and then replay them on a widget (in our case, the Calculator object).

Finally, as I built the test program in subfolder tests (under the example's folder) I was able to run it, giving me the following output:
********* Start testing of TestCalc *********
Config: Using QTest library 4.5.2, Qt 4.5.2
PASS   : TestCalc::initTestCase()
FAIL!  : TestCalc::testCalc(substract_integer) Compared values are not the same
   Actual (calc.result()): 0
   Expected (expected): -8
qtestcalc.cpp(73) : failure location
FAIL!  : TestCalc::testCalc(divide) Compared values are not the same
   Actual (calc.result()): 0
   Expected (expected): 2
qtestcalc.cpp(73) : failure location
PASS   : TestCalc::cleanupTestCase()
Totals: 2 passed, 2 failed, 0 skipped
********* Finished testing of TestCalc *********
We can see that two of the tests failed (more work for the developer to fix the issues!). One thing about tests output is that the qtestlib-tools project provides a set of tools for handling visualizing the data.

In summary, QTestLib module is a good way to develop useful and practical unit tests for applications developed using Qt.

Listing 1 - Changes to Calculator class
QString Calculator::result()
{
    return display->text();
}

void Calculator::keyPressEvent(QKeyEvent *e)
{
    bool pressButton = true;
    int i, row, column;

    switch (e->key()) {
    case Qt::Key_Enter:
    case Qt::Key_Return:
    case Qt::Key_Equal:
        row = 5;
        column = 5;
        break;
    case Qt::Key_0:
        row = 5;
        column = 1;
        break;
    case Qt::Key_1:
    case Qt::Key_2:
    case Qt::Key_3:
    case Qt::Key_4:
    case Qt::Key_5:
    case Qt::Key_6:
    case Qt::Key_7:
    case Qt::Key_8:
    case Qt::Key_9:
        i = e->text().toInt();
        row = ((9 - i) / 3) + 2;
        column = ((i - 1) % 3) + 1;
        break;
    case Qt::Key_Plus:
        row = 5;
        column = 4;
        break;
    case Qt::Key_Minus:
        row = 4;
        column = 4;
        break;
    case Qt::Key_Asterisk:
        row = 3;
        column = 4;
        break;
    case Qt::Key_Slash:
        row = 2;
        column = 4;
        break;
    case Qt::Key_Period:
        row = 5;
        column = 2;
        break;
    case Qt::Key_Backspace:
        row = 1;
        column = 0;
        break;
    case Qt::Key_Escape:
        row = 1;
        column = 4;
        break;
    default:
        pressButton = false;
        break;
    }
    if (pressButton) {
        QGridLayout *grid = qobject_cast<qgridlayout *>(layout());
        Button *button = qobject_cast<button *>(grid->itemAtPosition(row, column)->widget());
#ifndef CALC_NO_ANIMATE_BUTTONS
        button->animateClick();
#else
        button->click();
#endif
    }
}

Listing 2 - Test program
#include <QtGui>
#include <QtTest/QtTest>
#include "calculator.h"

class TestCalc: public QObject
{
Q_OBJECT

private slots:
    void testCalc_data();
    void testCalc();
};

void TestCalc::testCalc_data()
{
    QTest::addColumn<QTestEventList>("events");
    QTest::addColumn<QString>("expected");

    QTestEventList sum;
    sum.addKeyClick('1');
    sum.addKeyClick('+');
    sum.addKeyClick('2');
    sum.addKeyClick('=');
    QTest::newRow("sum") << sum << "3";

    QTestEventList substract;
    substract.addKeyClicks("10");
    substract.addKeyClick('-');
    substract.addKeyClick('9');
    substract.addKeyClick('=');
    QTest::newRow("substract") << substract << "1";

    QTestEventList substract_integer;
    substract.addKeyClicks("1");
    substract.addKeyClick('-');
    substract.addKeyClick('9');
    substract.addKeyClick('=');
    QTest::newRow("substract_integer") << substract_integer << "-8";

    QTestEventList times;
    times.addKeyClick('2');
    times.addKeyClick('*');
    times.addKeyClick('3');
    times.addKeyClick('=');
    QTest::newRow("times") << times << "6";

    QTestEventList divide;
    times.addKeyClick('8');
    times.addKeyClick('/');
    times.addKeyClick('4');
    times.addKeyClick('=');
    QTest::newRow("divide") << divide << "2";

    QTestEventList clear_display;
    times.addKeyClick('3');
    times.addKeyClick('4');
    times.addKeyClick('5');
    times.addKeyClick('\0x27');
    QTest::newRow("clear_display") << clear_display << "0";
}

void TestCalc::testCalc()
{
    QFETCH(QTestEventList, events);
    QFETCH(QString, expected);

    Calculator calc;
    calc.show();

    events.simulate(&calc);

    QCOMPARE(calc.result(), expected);
}

QTEST_MAIN(TestCalc)
#include "qtestcalc.moc"

No comments:

Post a Comment