GUI unit testing with Qt Test – part 2 – advanced testing

This tutorial explores more advanced topics in GUI unit testing with Qt Test, the Qt framework for C++ unit testing. A working example is discussed and analysed in detail. Full qmake project and C++ source code are provided.

More GUI unit testing with Qt Test

This tutorial will introduce more advanced features of Qt Test dedicated to GUI unit testing. In particular it will show how to simulate and handle keyboard focus and how to test Qt signals when unit testing a Graphical User Interface.

This is the fourth and final post of a series dedicated to Qt Test. The posts of this series are:

I recommend to read the previous posts of the series, in particular the first part of GUI unit testing with Qt Test, to fully understand new concepts I will introduce here.

Testing a QWidget

This tutorial will continue testing the QWidget introduced in part 1:

The widget contains 2 input fields and 2 buttons. Pushing the button “CONCAT” merges the string of the 2 input fields and prints the result to a label. Pushing the CANCEL button clears all the data.

The only difference with the widget implemented in part 1 is that this new version emits a signal every time a button is pushed. This is to experiment with signal testing as you will read soon.

Testing focus

When testing focus with Qt Test we are basically simulating an user interacting with our widget using a keyboard.

Something important to consider when working with focus is that simply calling QWidget::setFocus on a widget won’t work as the widget is not visible during the execution of a test. What we need to do to get things working is calling the static function QApplication::setActiveWindow on the QWidget we are testing, in this case the PanelConcat object:

void TestPanelConcat::TestFocus() { // enables focus and widget events QApplication::setActiveWindow(&panel);

From now focus events will be successfully delivered to our widget.

An extra step to take when testing a widget is to set which element will hold the focus initially with QWidget::setFocus:

// set initial focus panel.mInputA->setFocus(); QVERIFY2(panel.mInputA->hasFocus(), "Input A doesn't have focus");

After verifying that the focus is correctly set, we can move on with the simulation.

This animated GIF shows what we want to simulate. Note the blue highlight around the elements of the widget showing where the focus is.

The following code writes in the 2 input fields using the QTest::keyClicks function introduced in part 1. To move from one field to the other the code simulates the push of the TAB key, which by default moves the focus to the next widget in the focus chain. Finally the focus is moved to the CONCAT button which is pushed with the SPACE key.

// write STR1 QTest::keyClicks(QApplication::focusWidget(), STR1); // move focus to next widget QTest::keyClick(&panel, Qt::Key_Tab); QVERIFY2(panel.mInputB->hasFocus(), "Input B doesn't have focus"); // write STR2 QTest::keyClicks(QApplication::focusWidget(), STR2); // move focus to next widget QTest::keyClick(&panel, Qt::Key_Tab); QVERIFY2(panel.mButtonConcat->hasFocus(), "Button CONCAT doesn't have focus"); // press button CONCAT using space QTest::keyClick(QApplication::focusWidget(), Qt::Key_Space); QCOMPARE(panel.mLabelRes->text(), STR_RES);

The expected result of this whole simulation is to read the 2 strings merged in the result label.

The final part of the test will move the focus to the CANCEL button and it will push it:

// move focus to next widget QTest::keyClick(&panel, Qt::Key_Tab); QVERIFY2(panel.mButtonCancel->hasFocus(), "Button CANCEL doesn't have focus"); // press button CANCEL using space QTest::keyClick(QApplication::focusWidget(), Qt::Key_Space); QVERIFY2(panel.mInputA->text().isEmpty(), "Cancel didn't work on input A"); QVERIFY2(panel.mInputB->text().isEmpty(), "Cancel didn't work on input B"); QVERIFY2(panel.mLabelRes->text().isEmpty(), "Cancel didn't work on res label"); }

The expected result is an empty text in both the input fields and the result label.

I tested the behaviour of the two buttons in one single function for simplicity, but when writing real unit tests it’s better to keep things independent. Hence, in the real world, it would be better to split this test in two functions.

Testing signals

One of the key features of Qt is the object communication based on signals & slots, which basically is an event-based system.

To test that, Qt Test offers a class called QSignalSpy, which is a QList on steroids that can intercept and record signals.

As anticipated in the introduction, the PanelConcat class emits two signals:

DataAvailable(QString) – emitted after CONCAT is pushed and the input text has been merged.

– emitted after CONCAT is pushed and the input text has been merged. DataCleared() – emitted after CANCEL is pushed and all the text is cleared.

In the following test the code is going to simulate the push of the two buttons and to check the signals emitted.

The first part of the code sets the text directly in the input fields for simplicity. Then it creates two QSignalSpy objects and each is associated to a signal emitted by the panel object:

void TestPanelConcat::TestSignals() { // set input panel.mInputA->setText(STR1); panel.mInputB->setText(STR2); // create spy objects QSignalSpy spy1(&panel, &PanelConcat::DataAvailable); QSignalSpy spy2(&panel, &PanelConcat::DataCleared);

The second part simulates a push of the CONCAT button and then checks that a DataAvailable signal is recorded. Doing that is as simple as checking how many items the corresponding QSignalSpy object (spy1) contains. Then the last QCOMPARE checks that the parameter received from the signal corresponds to the result string.

// click button CONCAT QTest::mouseClick(panel.mButtonConcat, Qt::LeftButton); QCOMPARE(spy1.count(), 1); QCOMPARE(spy2.count(), 0); QList args = spy1.takeFirst(); QCOMPARE(args.at(0).toString(), STR_RES);

The last part of the test function simulates pushing the CANCEL button and checks that a DataCleared signal is received and that it doesn’t carry any parameter.

// click button CANCEL QTest::mouseClick(panel.mButtonCancel, Qt::LeftButton); QCOMPARE(spy1.count(), 0); QCOMPARE(spy2.count(), 1); args = spy2.takeFirst(); QVERIFY2(args.empty(), "DataCleared signal has parameters now?!?"); }

In this example I tested the 2 signals of PanelConcat in 1 test for simplicity, but it would have been better to keep things isolated in 2 different tests. Keep that in mind when writing real unit tests.

Source code

The full source code of this tutorial is available on GitHub and released under the Unlicense license.

The full project structure includes 3 sub-projects:

WidgetsLib – a dynamic library containing the widget class.

– a dynamic library containing the widget class. ExampleApp – an example application using the PanelConcat widget.

– an example application using the PanelConcat widget. TestPanelConcat – the unit test of PanelConcat.

To try the example load the top subdirs project called GuiUnitTestingAdv in Qt Creator.

Keep in mind that by default running the project will launch the example application. To run the unit tests you can change the active run configuration, use the Tests panel or use the menu

Tools > Tests > Run All Tests

References

To know more about the concepts introduced in this tutorial you can check out the latest documentation of the QTest namespace and of the QSignalSpy class.

If you want to learn more about Qt have a look at the other Qt tutorials I posted.

Conclusion

Writing unit tests for GUIs is usually more complicated than for “traditional” back-end/library code. That’s because you can’t simply test public functions and because there’s a stronger connection between elements. Nevertheless, as you can see from this and from my previous post, it is possible to simulate pretty much every kind of user interaction using Qt Test.

In case you need help to handle your GUI unit tests with Qt Test feel free to contact me.

Stay connected

Don’t forget to subscribe to the blog newsletter to get notified of future posts.

You can also get updates following me on Github, Google+, LinkedIn and Twitter.