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.
79
99%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
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),
xctest-mac-desktop (macOS), and at-spi-linux (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.