Введение
Логика, написанная непосредственно в QML и JavaScript (привязки свойств, обработчики событий, кастомные компоненты), нуждается в тестировании не меньше, чем код на C++, но QTest напрямую не предназначен для тестирования QML. Для этого Qt предоставляет отдельный модуль — Qt Quick Test, который запускает тестовые сценарии, написанные на QML с использованием TestCase API, аналогично QTest, но в контексте QML-движка. В статье покажем, как протестировать простой QML-компонент.
Концепция
Тесты Qt Quick Test пишутся как обычные QML-файлы с корневым элементом TestCase, импортированным из модуля QtTest. Каждая JavaScript-функция внутри TestCase, имя которой начинается с test_, становится отдельным тестовым случаем. Внутри тестов доступны привычные по духу QTest проверки (compare(), verify(), fail()), а также специализированные для QML возможности — программная симуляция пользовательского ввода через mouseClick(), keyClick() и ожидание изменения свойств через tryCompare() (полезно для проверки результатов анимаций и асинхронных привязок).
Пример кода
// Counter.qml — тестируемый компонент
import QtQuick
Item {
id: root
property int count: 0
function increment() {
count++;
}
function reset() {
count = 0;
}
Text {
id: label
objectName: "countLabel"
text: root.count.toString()
}
MouseArea {
anchors.fill: parent
onClicked: root.increment()
}
}
// tst_Counter.qml — тест на Qt Quick Test
import QtQuick
import QtTest
TestCase {
name: "CounterTests"
width: 200
height: 200
visible: true
Counter {
id: counter
anchors.fill: parent
}
function test_initialCountIsZero() {
compare(counter.count, 0);
}
function test_incrementIncreasesCount() {
counter.reset();
counter.increment();
compare(counter.count, 1);
}
function test_clickIncrementsCounter() {
counter.reset();
mouseClick(counter);
compare(counter.count, 1);
const label = findChild(counter, "countLabel");
compare(label.text, "1");
}
function test_multipleIncrementsAccumulate() {
counter.reset();
for (let i = 0; i < 5; i++) {
counter.increment();
}
tryCompare(counter, "count", 5);
}
}
# CMakeLists.txt — настройка запуска Qt Quick Test
find_package(Qt6 REQUIRED COMPONENTS QuickTest)
qt6_add_executable(tst_counter tst_counter_main.cpp)
target_link_libraries(tst_counter PRIVATE Qt6::QuickTest)
add_test(NAME tst_counter COMMAND tst_counter -input ${CMAKE_CURRENT_SOURCE_DIR})
Пояснения к коду
Компонент Counter.qml инкапсулирует простую логику счётчика с методами increment()/reset() и реакцией на клик мыши через MouseArea. Тестовый файл tst_Counter.qml создаёт экземпляр Counter непосредственно внутри TestCase и проверяет несколько сценариев: начальное состояние, программный вызов методов, реакцию на симулированный клик мыши через mouseClick(), и накопление значения через несколько вызовов с использованием tryCompare(), который повторяет проверку в течение заданного таймаута — полезно, если изменение свойства происходит асинхронно (например, через анимацию).
Функция findChild() находит дочерний элемент по objectName — удобный способ проверить состояние конкретного визуального элемента (текстовой метки) без необходимости хранить отдельную ссылку на него в коде теста.
Подводные камни
- Тестирование визуальных компонентов требует видимости (
visible: true) и ненулевых размеровwidth/height— без этого симуляция мыши черезmouseClick()не сработает корректно, поскольку координаты клика вычисляются относительно реальных геометрических размеров элемента. compare()сразу после изменения свойства, зависящего от анимации или привязки с задержкой, может провалиться из-за того, что движок QML ещё не успел пересчитать значение — в таких случаяхtryCompare()с разумным таймаутом надёжнее, чем мгновенныйcompare().- Состояние, не сброшенное между тестами. Поскольку экземпляр
Counterсоздаётся один раз для всего файлаTestCase(а не пересоздаётся перед каждымtest_-методом автоматически), важно явно вызыватьreset()в начале каждого теста, иначе тесты становятся зависимыми от порядка выполнения — то же правило изоляции, что и в QTest, но требующее ручного контроля в QML. - Запуск Qt Quick Test в headless CI также требует платформенного плагина
offscreenили виртуального дисплея, аналогично обычным GUI-тестам — без этого тесты, использующие реальный рендеринг или ввод мыши, не запустятся в стандартном CI-окружении без графического сервера.