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
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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).
QObject-based classes - view models,
data models, signal/slot graphs.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.QBENCHMARK
(qtns).QSignalSpy-based assertions on signal emission order /
arguments (qtidx).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.
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 outputThe 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.
Per qtns, three entry-point macros choose what application class the harness instantiates:
| Macro | Instantiates | Use for |
|---|---|---|
QTEST_MAIN | QApplication | Widget GUI tests |
QTEST_GUILESS_MAIN | QCoreApplication | Console / non-GUI logic tests |
QTEST_APPLESS_MAIN | none | Tests 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.
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.
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.
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.
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.
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,junitxmlThe -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.
./test_calculator -o results-junit.xml,junitxmlThe JUnit XML output feeds
junit-xml-analysis
for cross-platform aggregation alongside other JUnit-emitting test
runners.
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-pattern | Why it fails | Fix |
|---|---|---|
| Public slots as test functions | moc treats them as Qt signals/slots; harness ignores them | Private slots (qtover) |
Forgetting #include "tst_xxx.moc" for single-file tests | Linker error - moc output not bundled | Include the generated moc file at the bottom of the .cpp (Step 2) |
QTest::qWait(2000) between actions | Flaky; slow on fast machines, racy on slow | QTRY_VERIFY / QTRY_COMPARE with predicate polling (qtns) |
| One mega-test slot exercising many flows | First failure stops the chain; coverage attribution lost | One slot per behaviour; share setup via init() (qtover) |
Test depends on QTimer::singleShot(0, …) cascade | Event-loop ordering varies | Drive the event loop with QCoreApplication::processEvents() or QTRY_* predicates |
Using QTEST_MAIN for headless CI | Tries to instantiate QApplication without a display | QTEST_GUILESS_MAIN for non-widget tests; QT_QPA_PLATFORM=offscreen for widget tests (Step 10) |
| QSignalSpy connected after the action | Misses emissions; count is wrong | Construct QSignalSpy before the action that triggers the signal (Step 6) |
| Benchmarks mixed with correctness tests in the same slot | Iteration count masks regressions | Separate _benchmark() slots; gate on regression in CI |
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).QT_QPA_PLATFORM=offscreen or xvfb-run for widget-
level tests.QSignalSpy + QTRY_VERIFY or explicit event-loop
driving - clunkier than Playwright's await-everywhere model.winappdriver (Windows), qa-mobile's xcuitest-suite (macOS),
AT-SPI per desktop-test-strategy-reference (Linux).desktop-test-strategy-reference.junit-xml-analysis.