When to use
Use this when you need to author QML unit tests that exercise Qt Quick components, their properties, signals, and interactions from the QML side. Write one .qml test file per component under test and let Qt Quick Test discover the test_* functions.
Not for: running or triaging an existing suite and reading failures — see qt-qml-test-run. For diagnosing jank, binding storms, or slow startup at runtime — see qt-qml-profiler.
Method
- Create a test file named
tst_<component>.qmland addimport QtTestplus the module that exposes the component under test. - Add a root
TestCase { }element and set itsnameproperty to a unique identifier (the harness uses it to label results). - Decide how the component enters the scene. Decision point: if the component is stateless and cheap, instantiate it inline with a
Component { }+createTemporaryObject(); if it needs the running window (mouse/key focus), give theTestCaseawidth/heightand usewhen: windowShown. - Write each check as a function named
test_something(). Decision point: if you assert a single truth useverify()/compare(); if the value settles asynchronously (bindings, animations, timers) usetryCompare()/tryVerify()so the harness spins the event loop until timeout. - Observe signals with a
SignalSpy { }element bound totargetandsignalName; assertspy.countafter the triggering action, orspy.wait()for async emission. - Decision point: if a test repeats over inputs, add a matching
function test_something_data()returning table rows and readdataintest_something(data); otherwise keep it a plain function. - Add fixtures with
init()/cleanup()(per function) andinitTestCase()/cleanupTestCase()(once per file). Decision point: reset shared state ininit(); do one-time expensive setup ininitTestCase(). - Register the test with
qt_add_qml_moduleso imports resolve, and setTESTED_COMPONENTwhere you need the runner pointed at a specific element.
Example
import QtQuick
import QtTest
TestCase {
id: testCase
name: "SliderTests"
width: 200; height: 50
when: windowShown
Component {
id: sliderComp
Slider { from: 0; to: 100; value: 0 }
}
SignalSpy {
id: valueSpy
signalName: "valueChanged"
}
function init() { valueSpy.clear() }
function test_range_data() {
return [
{ tag: "min", input: 0, expected: 0 },
{ tag: "mid", input: 50, expected: 50 },
{ tag: "max", input: 100, expected: 100 },
];
}
function test_range(data) {
let s = createTemporaryObject(sliderComp, testCase);
valueSpy.target = s;
s.value = data.input;
compare(s.value, data.expected, "value should clamp to range");
tryCompare(valueSpy, "count", 1);
}
function test_click_emits() {
let s = createTemporaryObject(sliderComp, testCase);
valueSpy.target = s;
mousePress(s, s.width / 2, s.height / 2);
keyClick(Qt.Key_Right);
tryVerify(function() { return valueSpy.count > 0; });
verify(fuzzyCompare(s.value, s.value, 0.001));
}
}
Pitfalls
- Bad function names. Only functions matching
test_*()are collected;init,cleanup,initTestCase,cleanupTestCase, and*_dataare reserved hooks and are not run as tests. - Missing
when: windowShown.mousePress/keyClickand focus-dependent assertions silently do nothing until the window is shown; gate interactive tests on it. - Leaking objects. Prefer
createTemporaryObject(); objects fromcreateObject()persist across tests and pollute state. - Racing on async. Using
compare()on a value set by a binding or animation flakes; switch totryCompare()/tryVerify()which poll up to the timeout. - Stale SignalSpy. Reassigning
targetdoes not resetcount; callspy.clear()ininit(). - Data-row tag collisions. Duplicate
tagvalues in a_datatable make failures ambiguous; keep tags unique.
Output format
tst_<component>.qml
TestCase name: <Name>
Functions:
- test_<a>() [+ test_<a>_data() if data-driven]
- test_<b>()
Fixtures: init/cleanup [+ initTestCase/cleanupTestCase]
Spies: <SignalSpy targets>
CMake: qt_add_qml_module(<target> ...) ; TESTED_COMPONENT <Element>
Entry: QUICK_TEST_MAIN(<suite>) // in tst_main.cpp
Reference
import QtTestprovidesTestCase,SignalSpy, and the assertion helpers; it ships with the Qt Quick Test module (Qt6::QuickTest).TestCasehas anameproperty (labels the suite) andwhen(typically bound towindowShownfor interactive tests).- Test functions must be named
test_*(). Reserved hooks:initTestCase()/cleanupTestCase()run once per file;init()/cleanup()run around every test function. - Assertions:
compare(actual, expected, msg),verify(cond, msg),tryCompare(obj, prop, expected, timeout),tryVerify(fn, timeout),fuzzyCompare(a, b, delta)for floats. - Data-driven tests: define
function <name>_data()returning an array of row objects (each may carry atag); the matching<name>(row)receives one row per invocation. SignalSpyelement: settargetandsignalName, then readcount, callclear(), orwait(timeout).- Interaction helpers on
TestCase:mousePress/mouseRelease/mouseClick/mouseMove,keyClick/keyPress/keyRelease. Object lifecycle helpers:createTemporaryObject(),createTemporaryQmlObject(). QUICK_TEST_MAIN(name)(fromquicktest.h, thequick_test_mainentry point) in a small C++mainscans a directory oftst_*.qmlfiles and runs them.- Build wiring: use
qt_add_executablefor the test main andqt_add_qml_module(<target> URI <uri> VERSION 1.0 QML_FILES ...)so the tested module imports resolve; point the runner viaTESTED_COMPONENTwhen isolating one element. Verified against Qt 6.2–6.8.