CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/qt-test-framework

Authors and runs Qt Test - the first-party C++ unit + GUI test framework that ships with Qt 6 (via the `QtTest` module header). Covers the `QTEST_MAIN` / `QTEST_APPLESS_MAIN` / `QTEST_GUILESS_MAIN` entry-point macros, the `QObject` private-slot test pattern, `QVERIFY` / `QCOMPARE` / `QFETCH` assertions, GUI event simulation (`QTest::mouseClick`, `QTest::keyClick`, `QTest::touchEvent`), `QSignalSpy` for signal introspection, `QBENCHMARK` for performance regression, and the `-o file,junitxml` CI output. Use for in-process testing of Qt widgets, QObject signal/slot chains, and Qt Quick / QML application logic; for out-of-process Qt-app driving, use an OS-native accessibility driver instead.

74

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files
name:
qt-test-framework
description:
Authors and runs Qt Test - the first-party C++ unit + GUI test framework that ships with Qt 6 (via the `QtTest` module header). Covers the `QTEST_MAIN` / `QTEST_APPLESS_MAIN` / `QTEST_GUILESS_MAIN` entry-point macros, the `QObject` private-slot test pattern, `QVERIFY` / `QCOMPARE` / `QFETCH` assertions, GUI event simulation (`QTest::mouseClick`, `QTest::keyClick`, `QTest::touchEvent`), `QSignalSpy` for signal introspection, `QBENCHMARK` for performance regression, and the `-o file,junitxml` CI output. Use for in-process testing of Qt widgets, QObject signal/slot chains, and Qt Quick / QML application logic; for out-of-process Qt-app driving, use an OS-native accessibility driver instead.
metadata:
{"keywords":"qt, qttest, c++, qsignalspy, qbenchmark, widget-testing"}

qt-test-framework

Overview

Qt Test is a framework for unit testing Qt-based applications and libraries (qtover), providing standard unit-testing primitives plus Qt-specific extensions for GUI event simulation, data-driven tests, and benchmarking. The QTest namespace carries the verification macros (QVERIFY, QCOMPARE), data handling (QFETCH, QFETCH_GLOBAL), and entry points (QTEST_MAIN, QTEST_GUILESS_MAIN, QTEST_APPLESS_MAIN).

Qt Test is in-process - the test executable links the Qt application code and emits events directly into the QObject event queue. It does not go through the OS accessibility tree (per desktop-test-strategy-reference), so it cannot drive a Qt app from a separate process. For out-of-process Qt driving see winappdriver (Windows via UIA after QAccessible is enabled), qa-mobile's xcuitest-suite with its references/macos.md (macOS), and the AT-SPI sections of desktop-test-strategy-reference (Linux).

When to use

  • Pure-logic unit testing of QObject-based classes - view models, data models, signal/slot graphs.
  • GUI tests of QWidget or Qt Quick views where the test executable can link the app code (in-process), avoiding the brittleness of out-of-process accessibility-tree drivers.
  • Benchmark + performance-regression checks via QBENCHMARK (qtns).
  • QSignalSpy-based assertions on signal emission order / arguments (qtidx).

Step 1 - Add Qt Test to the build

CMake (Qt 6, the canonical Qt build system per Qt 6 docs):

find_package(Qt6 6.5 REQUIRED COMPONENTS Test Widgets)

qt_add_executable(test_calculator tst_calculator.cpp Calculator.cpp)
target_link_libraries(test_calculator PRIVATE Qt6::Test Qt6::Widgets)
add_test(NAME test_calculator COMMAND test_calculator)

qt_add_executable runs moc automatically, which Qt Test slot discovery depends on.

Step 2 - Author a test class

The canonical shape per qtover - a QObject subclass with private slots as test functions:

#include <QtTest/QtTest>
#include "Calculator.h"

class TestCalculator : public QObject {
    Q_OBJECT

private slots:
    // initTestCase() / cleanupTestCase() run once per class
    void initTestCase();
    void cleanupTestCase();
    // init() / cleanup() run around each test function
    void init();
    void cleanup();

    void addsTwoIntegers();
    void emitsResultChangedSignal();
    void rejectsDivisionByZero();
};

void TestCalculator::initTestCase() {
    qDebug() << "Starting test suite";
}

void TestCalculator::addsTwoIntegers() {
    Calculator c;
    QCOMPARE(c.add(2, 3), 5);     // strict equality assertion
    QVERIFY(c.lastError().isEmpty());
}

QTEST_MAIN(TestCalculator)        // generates main() + QApplication
#include "tst_calculator.moc"     // include moc output

The four lifecycle slots are recognised by name (qtover): initTestCase (once before any test), cleanupTestCase (once after), init (per test), cleanup (per test).

Verify: build the target and run ./test_calculator -functions; confirm it lists your private-slot test functions before authoring data-driven or GUI cases. If none appear, the slots are not under private slots: or the #include "tst_*.moc" line is missing - fix and rebuild.

Step 3 - Pick the right entry-point macro

Per qtns, three entry-point macros choose what application class the harness instantiates:

MacroInstantiatesUse for
QTEST_MAINQApplicationWidget GUI tests
QTEST_GUILESS_MAINQCoreApplicationConsole / non-GUI logic tests
QTEST_APPLESS_MAINnoneTests of code that itself instantiates its own application object

Per qtover, if the test class defines a static public void initMain() method, "it is called by the QTEST_MAIN macros before the QApplication object is instantiated" - that's the hook for setting platform-specific environment variables before Qt's event loop starts.

Step 4 - Data-driven tests

Per qtns, QFETCH retrieves test data values; data is declared in a sibling _data() slot:

private slots:
    void addsTwoIntegers_data();
    void addsTwoIntegers();

void TestCalculator::addsTwoIntegers_data() {
    QTest::addColumn<int>("a");
    QTest::addColumn<int>("b");
    QTest::addColumn<int>("expected");

    QTest::newRow("zeros")       << 0 << 0  << 0;
    QTest::newRow("positives")   << 2 << 3  << 5;
    QTest::newRow("negatives")   << -2 << -3 << -5;
    QTest::newRow("overflow")    << INT_MAX << 1 << INT_MAX + 1;  // documents UB
}

void TestCalculator::addsTwoIntegers() {
    QFETCH(int, a);
    QFETCH(int, b);
    QFETCH(int, expected);
    Calculator c;
    QCOMPARE(c.add(a, b), expected);
}

Per qtover: "A test can be executed multiple times with different test data." Each newRow runs the test function once.

Step 5 - GUI event simulation

The QTest namespace provides keyboard (keyClick / keyClicks), mouse (mouseClick / mousePress), touch (touchEvent), and wheel event helpers (qtns); the full function-family table is in references/qt-gui-signal-benchmark.md.

void TestLoginWidget::successfulLogin() {
    LoginWidget w;
    w.show();
    QVERIFY(QTest::qWaitForWindowExposed(&w));

    QTest::keyClicks(w.usernameField(), "alice");
    QTest::keyClicks(w.passwordField(), "s3cret");
    QTest::mouseClick(w.submitButton(), Qt::LeftButton);

    QTRY_VERIFY(w.isLoggedIn());   // polls until true or times out
    QCOMPARE(w.currentUser(), QStringLiteral("alice"));
}

QTRY_VERIFY / QTRY_COMPARE (qtns) poll the predicate with a default 5-second timeout - the right primitive for waiting on async signal/slot completion without ad-hoc QTest::qWait sleeps.

Step 6 - Signal introspection with QSignalSpy

Per qtidx, QSignalSpy enables "easy introspection for Qt's signals and slots":

void TestCalculator::emitsResultChangedSignal() {
    Calculator c;
    QSignalSpy spy(&c, &Calculator::resultChanged);

    c.add(2, 3);

    QCOMPARE(spy.count(), 1);
    const QList<QVariant> args = spy.takeFirst();
    QCOMPARE(args.at(0).toInt(), 5);
}

This is the canonical pattern for asserting on signal emission order, count, and argument values - far more robust than connecting test-internal slots and counting invocations by hand.

Step 7 - Benchmarks

QBENCHMARK executes a block repeatedly to measure performance, reporting CPU time, walltime, or instructions-retired per the active back-end; use QBENCHMARK_ONCE for a single run (qtns, qtover). Example slot: references/qt-gui-signal-benchmark.md.

Step 8 - Run

Per qtover, a Qt Test executable accepts the following command-line options:

# List all test functions
./test_calculator -functions

# Extended verbose - shows each QCOMPARE / QVERIFY
./test_calculator -v2

# Run a specific test function
./test_calculator addsTwoIntegers

# Run a specific data row
./test_calculator addsTwoIntegers:negatives

# Write JUnit XML for CI ingestion
./test_calculator -o results.xml,junitxml

The -o filename,format flag per qtover supports formats: "txt, csv, junitxml, xml, lightxml, teamcity, or tap".

For multi-binary suites, ctest (driven by add_test from Step 1) runs the per-test executables and aggregates outcomes.

Step 9 - Parsing results

./test_calculator -o results-junit.xml,junitxml

The JUnit XML output feeds junit-xml-analysis for cross-platform aggregation alongside other JUnit-emitting test runners.

Step 10 - CI integration

Run the ctest suite across an Ubuntu / Windows / macOS matrix; Linux needs QT_QPA_PLATFORM=offscreen (the headless Qt platform plugin) for GUI-touching executables with no X / Wayland session. Full workflow: references/qt-gui-signal-benchmark.md.

Anti-patterns

Anti-patternWhy it failsFix
Public slots as test functionsmoc treats them as Qt signals/slots; harness ignores themPrivate slots (qtover)
Forgetting #include "tst_xxx.moc" for single-file testsLinker error - moc output not bundledInclude the generated moc file at the bottom of the .cpp (Step 2)
QTest::qWait(2000) between actionsFlaky; slow on fast machines, racy on slowQTRY_VERIFY / QTRY_COMPARE with predicate polling (qtns)
One mega-test slot exercising many flowsFirst failure stops the chain; coverage attribution lostOne slot per behaviour; share setup via init() (qtover)
Test depends on QTimer::singleShot(0, …) cascadeEvent-loop ordering variesDrive the event loop with QCoreApplication::processEvents() or QTRY_* predicates
Using QTEST_MAIN for headless CITries to instantiate QApplication without a displayQTEST_GUILESS_MAIN for non-widget tests; QT_QPA_PLATFORM=offscreen for widget tests (Step 10)
QSignalSpy connected after the actionMisses emissions; count is wrongConstruct QSignalSpy before the action that triggers the signal (Step 6)
Benchmarks mixed with correctness tests in the same slotIteration count masks regressionsSeparate _benchmark() slots; gate on regression in CI

Limitations

  • In-process only. Qt Test links the application code into the test binary; it cannot drive a separately-packaged Qt app - for that, use an OS-native accessibility driver.
  • No binary compatibility guarantee. Per qtidx, "no binary compatibility guarantee" is offered for Qt Test; recompile when the Qt version changes.
  • QApplication singleton constraint. Multiple QTEST_MAIN test binaries cannot share a process - each test binary runs as its own executable (which is why ctest exists).
  • GUI tests need a display platform. Headless Linux runners require QT_QPA_PLATFORM=offscreen or xvfb-run for widget- level tests.
  • No async-await ergonomics. Qt Test predates coroutines; async flows use QSignalSpy + QTRY_VERIFY or explicit event-loop driving - clunkier than Playwright's await-everywhere model.
  • Localised string assertions in QCOMPARE are brittle - prefer asserting on QString identifiers (object names) or enum values rather than displayed text.

References

  • Qt Test overview - qtover.
  • QTest namespace reference - qtns.
  • Qt Test module index - qtidx.
  • Sibling out-of-process Qt drivers: winappdriver (Windows), qa-mobile's xcuitest-suite (macOS), AT-SPI per desktop-test-strategy-reference (Linux).
  • Strategic frame: desktop-test-strategy-reference.
  • Downstream: junit-xml-analysis.
Workspace
testland
Visibility
Public
Created
Last updated
Publish Source
GitHub
Badge
testland/qt-test-framework badge