Qt comes with QtTest library, which lets you write your own test suites for internal and GUI components. However, its usage together with your application in an easy to maintain way is not so obvious. In this tutorial I’ll focus more on framework and ease of usage and less on how to write contents of tests themselves. There is already a tutorial on how to write QtTest unit tests. I’m using Qt 4.8 for this tutorial, but those methods and concepts can be used in Qt 5 too.

Some code to test

I created an example project, which is a simple math expressions parser with GUI. I’m sharing it under Public Domain license on bitbucket, so use is as you please. There are three non-GUI classes there:

  • Token – class representing a single entity in math expressions – a number or an operator (+, -, *, /),
  • MathTokenier – class used to split a string into tokens while validating input,
  • MathParser – class used to parse and compute expression consisting of sequence of tokens.

Writing test suites

Having some decent chunk of code, we can now move on to testing it. Tests are grouped into test suites. Test suite is represented by a normal QObject class with test functions being her slots. There’s a special naming convention used here:

  • Tests have “test” prefix of their method name,
  • initTestCase() and cleanupTestCase() are methods called before and after execution of the whole test suite,
  • init() and cleanup() are methods called before and after execution of each test in the suite.

Structure is as below:

#include <QtTest>

class TestSomething : public QObject {
    Q_OBJECT

private slots:
    // functions executed by QtTest before and after test suite
    void initTestCase();
    void cleanupTestCase();

    // functions executed by QtTest before and after each test
    void init();
    void cleanup();

    // test functions - all functions prefixed with "test" will be ran as tests
    void testSomething();
};

Any function not conforming to those name conventions will be just ignored and not ran as test. So we can safely create helper functions. I’d like to note that this whole mechanism work well thanks to QObject’s meta properties which lets you list and access all your slots dynamically (and much more).

Lets create such a test suite in test/ folder (or some other location). It’s a good idea to write some stubs at the beginning to see if everything works well. So lets do this:

void TestTriangleIntegral::testStubPass() {
    QVERIFY(true);
}

void TestTriangleIntegral::testStubFail() {
    QVERIFY(false);
}

After setting everything up well, two messages should pop out after running those tests:

PASS   : TestSomething::testStubPass()
FAIL!  : TestSomething::testStubFail() 'false' returned FALSE.

Running test suites

QtTest library is used by making a separate executable that runs your tests. If you’d do this by creating another project, you’d have to maintain two lists of source files, libraries, flags etc. I’ll describe the approach I’ve taken to avoid that.

The default way is to use QTEST_MAIN macro, but that lets you use just one test suite per executable. Lets use another approach by creating a main() function for tests in test/main.cpp in the following fashion:

#include <QtTest>
#include "testsuite1.h"
#include "testsuite2.h"

int main(int argc, char** argv) {
    QApplication app(argc, argv);
    QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8"));
    QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));

    TestSuite1 testSuite1;
    TestSuite2 testSuite2;
    // multiple test suites can be ran like this
    return QTest::qExec(&testSuite1, argc, argv) |
            QTest::qExec(&testSuite2, argc, argv);
}

Note usage of “|” instead of “||”. This is in order to run all tests, even if one of test suites failed.

That’d be enough to run a test suite if it was a standalone application. But since we don’t want to maintain two applications, we’ll share the build between them.

Lets create a new Build Configuration to produce unit tests executable while also including to the build all code used in original application. Go to Projects > Build Settings and duplicate your debug configuration naming the new one “Test”. Then go to Build Steps, select qmake step and add additional argument “CONFIG+=test”. “Additional arguments” is a place in which you can define some additional parameters for your build. We’ll be using this together with conditionals in Qt project file (.pro) to create test build.
qt-config-test
Note: this step has to be repeated for each checkout of your project, as those settings are stored in “.pro.user” file (which shouldn’t be checked in as it contains machine-specific build information).

QtTest application differs from our application only by using different main function and having additional test suite classes. If you didn’t put much code into your main function (which should have been the case), you should be fine with just removing it from test build. You can use the “test” configuration argument defined earlier to incorporate that. Lets create the test build by:

  • adding QtTest library,
  • renaming our target executable to one with test name (optional),
  • removing original main.cpp file (in order to avoid conflicts),
  • adding sources of our test suites.

Assuming main.cpp contains your original main function, test/main.cpp contains main function for tests and test/testSuite* contain test suites, you can use following code:

test {
    message(Test build)
    QT += testlib
    TARGET = UnitTests

    SOURCES -= main.cpp

    HEADERS += test/testSuite1.h \
        test/testSuite2.h

    SOURCES += test/main.cpp \
        test/testSuite1.cpp \
        test/testSuite2.cpp
} else {
    message(Normal build)
}

In case of normal build it only displays message. But in case of test (CONFIG+=test) build, it also does changes mentioned above.

Writing test methods

Content of those tests is just like normal test cases – initialize some data, run tested code and check if everything is okay. I won’t go into details – QtTest tutorial elaborates enough on them. Just remember to browse through available macros and use QVERIFY/QVERIFY2 and QCOMPARE instead of Q_ASSERTs. See test/testMathTokenizer.cpp in my example project for examples of test methods themselves. If you want to write more exhaustive tests, have a look at Qt’s data-driven tests. There are also GUI tests, but those work well mainly on widgets, as your main window usually has private access to UI elements.

This setup allows for development in which you often write tests and run them, but is not quite as convenient as to hit some key combination to run your tests and display red/green. Also switching between builds (Debug/Test) is required if you want to alternate between running tests and application. So test-driven development is possible, although requires more work than in other libraries/languages.

5 Comments

  1. Pingback: Customizing Qt project files (.pro) | Xilexio's lair

  2. Thanks very much! This is a smart way for adding unit tests. QtCreator definitely needs some improvements on that. It makes things easier doing it your way, rather than using a subdir project or completely independet projects. Because you don’t have to take care of header and object file inclusion into your unit test project.

    Reply

  3. Thank you a lot!

    Reply

  4. Great tutorial man. Thank you a lot!!!

    Reply

  5. Elegant solution to an annoying inconvenience of the Qt framework.

    But I have the problem that for some reasons my usual main.cpp is still used. Only when I rename the original file, it uses the test/main.cpp

    Any hints?

    Reply

  6. SpaghettiCat

    Thank you for this tutorial. The problem I had was after building the “Test” build, then switching to “Debug” build, trying to build the “Debug” build fails giving warnings like “using old main.o recipe” and errors that point to the test files (which shouldn’t have been built in the “Debug” build.

    The fix was to rename “tests/main.cpp” -> “tests/test_main.cpp” (and reflect the changes in the .pro file), as apprently the “main.cpp” of the Debug build and of the Test build were confusing the compiler.

    Reply

Leave a Reply

Your email address will not be published.