QML

Тестирование QML-кода: Qt Quick Test на практике

1 просмотров

Введение

Логика, написанная непосредственно в 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-окружении без графического сервера.