QtTest is a unit testing framework that comes with Qt. It provides classes and methods for building test fixtures to test units of code. QtTest allows testing signals without needing to write slots using QSignalSpy. It can also simulate user events like mouse clicks and key presses. While QtTest is useful for unit testing, it does not provide isolation of dependencies between units. Tests using QtTest should focus on testing one thing at a time and creating new objects for each test to isolate the code being tested.
2. Qt is the Kitchen Sink Toolkit
• Qt comes with just about everything you need
• SQL, Sockets, XML, GUI, etc
• Often one of the challenges of using Qt is knowing all the
modules
• And what they do!
• This is ICS performs these webinars!
• Qt comes with it’s own Unit Testing Framework
• QtTest
3. Types of Testing
• Unit Testing
• Test one discrete area of the software
• Usually on a class basis. Think “Test Piston Rod”
• Tests can be written and performed as code is developed
• White box tests. Often written by developers
• “Does the software perform as the developer intended?”
• Functional Testing
• Tests subsystems of the whole software system
• Usually a group of classes. Think “Test Engine”
• Tests can be preformed once subsystem is wired together
• Tests interaction between specific groups of units
• “Does the software work together?”
4. Types of Testing
• System Testing
• End to end testing of the whole software system
• Sometimes includes actual hardware. Think “Test Car”.
• Tests are written late in project
• Because of the end to end nature.
• Black box tests. Often written by separate SQA team
• “Does the software do right thing?”
5. Test Frameworks for Qt
• QTest / QTest-QML
• Unit testing framework that comes with Qt
• Qt specific functionality
• Can test blind signals. Can simulate mouse and key events.
• Qt specific features can be used outside of QTest framework
• Google Test
• Unit testing framework from Google
• Unique ability to do “Isolated Testing” via Google Mock
• Requires “Inversion of Control” pattern
• Squish
• Playback / Record frame work for “end to end” system testing.
• Used for System Testing
6. Benefits of Unit Testing
• Test Early!
• Find errors while code is actively being developed
• Rather than long investigations during system testing
• Test Often!
• Run unit tests per commit. Or even pre-commit!
• Any failures must have been introduced by this 200 lines of code!
• Test for Regressions!
• Did new functionality or bug fixing break existing functionality?
• What tests failed?
• Did a code change re-introduce a bug?
• Every bug can have it’s own set of unit tests.
7. Design for Testability
• Your code must be testable!
• There are design techniques to help make your code more testable
• Try not to add functions to production code just for tests
• Subclassing for tests is acceptable (.ui file members)
• Isolation of units is your primary goal
• Can I test just this class?
• Without re-testing lower level classes?
• Also think about error conditions
• Some may be hard to produce in production environments
• Can I stimulate a bad MD5 error?
• Socket disconnects?
• Default switch cases?
8. Build Application as Libraries
• Building the bulk of your application as a set of libraries
increases testability
AppBackend.dll
App.exe
(main.cpp)
AppUi.dll
BackendTests.exe UiTests.exe
C++ Test Fixtures QML Test Fixtures
9. Layered Design
• A layered design is more testable
• Easy to isolate lower layers for testing
• Test Communications without Data, Presentation or Visualization
• No upwards dependencies
• Hard downward dependencies
Visualization Layer (QML)
Presentation Layer (QObjects)
Data Layer (Local Data Storage)
Communications Layer (TCP)
Calls
Down
Signals
Up
10. Break Downward Dependencies
• Dependency injection works well
• An Inversion of Control Pattern
• Classes take their dependencies as constructor arguments
• Classes do not construct any members themselves
• Loose coupling with signals and slots also works well
• Classes interface to each other via signals and slots only
• 3rd class wires say the UI classes to the Backend classes
• Lets you substitute a test fixture object for a dependency
object
• Instead of an actual production object.
11. Why is isolation important?
• Avoids testing the same code over and over
• You have already tested the data model
• Why do the UI tests need to also test the model?
• This will make your tests run very quickly
• If there is a failure in a low level class it won’t trigger errors in higher
level tests
• Avoid testing side effects
• Tests can be implementation agnostic
• DB, Files, Cloud. Shouldn’t matter for UI tests.
• Tests should only depend on interfaces, not behavior
12. EE Analogy: Motor Controller Test
Motor Controller
7408 AND
Momentary
Switch
Safety
Interlock
Motor
1
2
3
Stimulus: Apply PowerVerify: High or Low
13. QtTest is a unit testing framework
• QtTest comes with classes to build test fixtures for units
• Has some “special sauce” for Qt
• Test signals without needing to write slots (QSignalSpy)
• Simulate user events (MousePress, KeyPress, FocusIn, etc)
• QtTest is not an isolation framework
• You will have to make replacement classes for dependencies
yourself
• These are usually referred to as Mock Classes.
• Next month we will talk about Google Mock which is can help a lot
with isolation framework.
14. Creating a Test Fixture
• Can be implemented inside a source file
• No need for a header file
• Inherit from QObject
• Qt uses introspection to deduce test functions
• Private slots are test functions
• There are some reserved function names
• Use Q_TEST_MAIN macro to generate a main()
• Or create you own main.cpp to run multiple test fixtures
15. Creating a Test Fixture
#include <QtTest>
class TestQString: public QObject
{
Q_OBJECT
private slots:
void toUpper() {
QString str = "Hello"; // Prepare
str = str.toUpper(); // Stimulate
QCOMPARE(str, QString("HELLO")); // Verify
}
};
QTEST_MAIN(TestQString)
#include “testqstring.moc”
16. Test Fixture Project
QT += testlib # Plus libs your uses.
CONFIG += testcase # Creates make check target
SOURCES += TestQString.cpp
qmake
make
make check
********* Start testing of TestQString *********
Config: Using QtTest library %VERSION%, Qt %VERSION%
PASS : TestQString::initTestCase()
PASS : TestQString::toUpper()
PASS : TestQString::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of TestQString *********
17. Running Multiple Tests
#include “QTestString.h” // Easier if you use a header file
int main(int argc, char** argc)
{
bool success = true;
r &= QTest::qExec(TestQString(), argc, argv);
...
return success;
}
18. Initializing and Cleaning Up
class TestQString: public QObject
{
Q_OBJECT
private slots:
void initTestCase() { // Called before ALL tests. }
void cleanupTestCase() { // Called after ALL tests}
void init() { // Called before each test. }
void cleanup() { // Called after each test }
void toUpper();
void toLower();
};
19. Writing Good Tests
• Test one thing at a time
• It’s tempting to get as much done in one function as you can.
• This makes one verification depend on another verification
• Tests will be brittle and changes to code could break lots of
verifications
• Do not have your tests depend on each other
• Order should not matter!
• Not depending on other tests will help you when you remove
functionality.
• You do not want to have to fix a cascading waterfall of tests!
20. Use New Object For Each Test
• Use init()
• Create your Object Under Test
• Plus whatever dependencies it has
• Production dependencies
• Or Test dependencies like Mock Classes
• Use cleanup()
• Do not be sloppy and leak memory in you tests.
• Your tests should run clean under Valgrind
• Aside for some suppressions for FreeType and FontConfig
21. Use New Object For Each Test
class MotorControllerTests: public QObject
{
Q_OBJECT
private slots:
void init() { controller = new MotorController; }
void cleanup() { delete controller; }
void motorRunsIfInterlockAndSwitchAreTrue() {
controller.setInterLock(true);
controller.setSwitch(true);
QCOMPARE(controller.motorOn(), true)
}
void motorDoesNotRunIfInterlockIsFalse() { ... }
private:
MotorController* controller;
};
22. Data Driven Tests
• Sometimes you need to test many inputs to a function
• NULL, Same Value Twice, Negative, Positive, NAN, etc
• This isn’t great test code. Violates Test 1 Thing
QCOMPARE(QString("hello").toUpper(), QString("HELLO"));
QCOMPARE(QString("Hello").toUpper(), QString("HELLO"));
QCOMPARE(QString("HellO").toUpper(), QString("HELLO"));
QCOMPARE(QString("HELLO").toUpper(), QString("HELLO"));
• Or worse this. Lots of error prone typing if not trivial.
void testhelloToUpper() {...}
void testHELLOToUpper() {...}
void testHeLOToUpper() {...}
23. Data Driven Tests
private slots:
void toUpper_data() {
QTest::addColumn<QString>("string");
QTest::addColumn<QString>("result");
//We can name the sub tests
QTest::newRow("all lower") << "hello" << "HELLO";
QTest::newRow("mixed") << "Hello" << "HELLO";
QTest::newRow("all upper") << "HELLO" << "HELLO";
}
void TestQString::toUpper() { // Called 3 Times
QFETCH(QString, string);
QFETCH(QString, result);
QCOMPARE(string.toUpper(), result);
}
};
24. Testing Events and Signals
• Use QSignalSpy to attach to any signal
• Signal parameters need to be QVariant compatible
• May require Q_DECLARE_METATYPE()
• May require qRegisterMetaType<>()
• Verify with count() and arguments()
• Use QtTest static methods to stimulate events
• void mousePress(widget, button, modifiers, pos)
• void keyClicks(widget, string, modifiers)
• Etc, etc
25. Testing Events and Signals
class QPushButtonTests: public QObject
{
Q_OBJECT
private slots:
void init() { //Create, Show, Wait For Window Shown }
void emitsClickedWhenLeftMouseButtonIsClicked() {
QSignalSpy spy(button, &QPushButton::clicked);
QTest::mouseClick(button, Qt::LeftButton)
QCOMPARE(spy.count(), 1);
}
private:
QPushButton* button;
};
26. Stimulating Signals
• Often your object under test is depending on signals from
other objects.
• For example QPushButton clicked()
• Don’t re-test the button
• Directly call QPushButton clicked()
• signals access specifier is actually a #define public
27. Stimulating Signals
private slots:
void init() {
settings = new Settings;
settingsDialog = new SettingsDialog(settings);
}
void LanguageComboBoxUpdatesOnSettingsLanguageChanged() {
QComboBox* combo = settings->ui()->langCombo;
settings->lanuageChanged(“en_US”);
QCOMPARE(combo->currentText(), “English”);
}
private:
Settings* settings;
TestableSettingsDialog* button; // Exposes ui member via function
};
28. Testing QML with QTest
• Writing tests in QML is very similar to C++
• Create a test fixture class
• Implement specially named functions
• Prepare, Stimulate, Verify.
• KISS – Keep It Short and Simple
• There are QML equivalents to
• compare, verify, wait
• SignalSpy
• mouseClick(...)
• TestCase
• Specific test fixture class. C++ just uses QObject.
29. UI Control Test
• UI Controls are easy to test as they have no dependencies
TestCase {
name: "Button“
Button { id: button }
SignalSpy { id: clickedSpy; target: button; signal: ”clicked” }
function test_mouseClickEmitClicked() //Tests are run in alpha order
{
mouseClick(button)
compare(clickedSpy.count, 1)
}
}
30. New Objects For Each Test
TestCase {
id: testCase
property Button button: null
name: "Button“
Component { id: buttonFactory; Button{} }
function init() { button = buttonFacotry.createObject(testCase) }
function cleanup() { button.destroy() }
function test_mouseClickEmitClicked()
{
mouseClick(button)
compare(clickedSpy.count, 1)
}
}
31. Running QML Tests
• qmltestrunner
• Prebuilt test runner.
• Takes a filename or directory of files to run
• Many command line options
• See Qt Documentation
• Can run many test by itself.
• All UI controls should be testable this way
• Custom main.cpp runner
• Reuse the guts of qmltestrunner
• Add injected type information or other setup
• As required by your app
33. Custom main.cpp
#include <QtQuickTest/quicktest.h>
#include <QtCore/qstring.h>
#ifdef QT_OPENGL_LIB
#include <QtOpenGL/qgl.h>
#endif
#define TEST_DIR “UiTests” // Pass this as DEFINE in project file
int main(int argc, char **argv)
{
//Register Types Here
return quick_test_main(argc, argv, “UiTests", TEST_DIR);
}
34. Screen Tests
• Screens are more interesting
• They depend on Controllers and Models from C++
• QML and C++ is natively a layered architecture
• Most common ways to communicate with C++ is injection
• Of C++ instances or C++ types
• However we can Mock Controllers and Models
• Using QML instead of C++
35. C++ Integration Mechanisms
• Context Properties – Injects global pointers at root context
• view.rootContext()->setContextProperty(“coffeeMaker”, &maker);
• QML Singletons – Inject a singleton factory for import
• qmlRegisterSingletonType<CoffeeMaker>(“MrCoffee", 1, 0,
“CoffeeMaker",
factoryFunction);
• Registered Types – Add new types for import
• qmlRegisterType<CoffeeMaker>(“MrCoffee“, 1, 0, “CoffeeMaker“);
36. Mocking Context Properties
• Create a Mock[ClassName].qml file
• Create the equivalent property, signal and function API
• The TestCase item is the root item for your test
• It literally is the rootContext()
• Simply add properties to the TestCase with the same names
• They will have global scope
• Tests can be run through the standard qmltestrunner
37. Mocking Singletons
• Create a Mock[ClassName].qml file
• Create the equivalent property, signal and function API
• Create a custom QML Test main.cpp
• Call qmlRegisterSingletonType with URL to file name of mock
• qmlRegisterSingletonType(QUrl("file:///path/MockCoffeeMaker.qml"),
“MrCoffee", 1, 0,
“CoffeeMaker");
• Run tests as usual.
• Objects will use mock versions of C++ classes
38. Mocking Registered Types
• Create a Mock[ClassName].qml file
• Create the equivalent property, signal and function API
• Inside a directory structure similar to production
• Mocks/com/ics/qtest/examples/
• Create a qmldir manifest file listing your mock types
• CoffeeMaker 1.0 MockCoffeeMaker.qml
• Manipulate QML2_IMPORT_PATH to pull in testing
module rather than production module
42. CoffeeScreen.qml With Hooks
Screen {
property alias _spinBox: spinBox // No QML protected specifier
property alias _button: button
...
SpinBox {
id: spinBox
value: coffeeMaker.targetTemp
onValueChanged: coffeeMaker.targetTemp = value
Connections {
target: coffeeMaker
onTargetTempChanged: value = coffeeMaker.targetTemp
}
}
Button {
id: button
text: qsTr(“Brew”)
onClicked: coffeeMaker.brew(CoffeeMaker.Medium)
}
}
43. Only Test CoffeeScreen
• CoffeeMaker is tested separately
• Is a discrete unit
• Has it’s own unit tests.
• A broken CoffeeMaker object shouldn’t fail CoffeeScreen tests
• UI Controls are tested separately
• SpinBox and Button are also discrete units
• Have their own tests
• Changing UI control APIs could cause tests not to run
• Controls are leaf nodes in the software design
• Easy to test by themselves.
• Should stabilize quickly
• When was the last time the API for SpinBox changed?