Различные статьи по Qt и c++

Тестирование PySide6-приложений: pytest-qt

Введение

Основной цикл статей про тестирование (статья 6) и Qt Quick Test для QML (статья 496) разбирали тестирование на C++/QML — в этой статье разберём тестирование PySide6-приложений через pytest-qt, плагин для популярного Python-фреймворка тестирования pytest, специально адаптированный для особенностей событийного цикла Qt и сигналов/слотов.

Концепция

pytest-qt предоставляет фикстуру qtbot, дающую методы для симуляции пользовательского взаимодействия (клики, нажатия клавиш — аналогично mouseClick/keyClick из Qt Quick Test статьи 496, но для Qt Widgets, а не QML) и для явного, надёжного ожидания срабатывания сигналов (qtbot.waitSignal()) — что особенно важно для тестирования асинхронного, событийно-управляемого кода Qt, где простая, прямая проверка результата немедленно после действия может не сработать корректно, если результат становится известен лишь спустя некоторое время через сигнал.

Пример кода: Тестирование PySide6-приложений

# pytest-qt тест для простого виджета — аналог теста CustomButton из статьи 496 (но для Qt Widgets)
import pytest
from PySide6.QtWidgets import QPushButton

def test_button_click_changes_label(qtbot):
    button = QPushButton("Нажми меня")
    qtbot.addWidget(button)  # регистрация виджета для автоматической очистки после теста

    clicked_count = {"value": 0}
    button.clicked.connect(lambda: clicked_count.__setitem__("value", clicked_count["value"] + 1))

    qtbot.mouseClick(button, Qt.MouseButton.LeftButton)  # симуляция реального клика мыши

    assert clicked_count["value"] == 1
# Ожидание сигнала через waitSignal — корректное тестирование асинхронного кода
import pytest
from PySide6.QtCore import QTimer

def test_delayed_signal_eventually_fires(qtbot):
    class DelayedNotifier(QObject):
        notified = Signal()
        def trigger_after_delay(self):
            QTimer.singleShot(200, self.notified.emit)  # сигнал сработает через 200мс, а не немедленно

    notifier = DelayedNotifier()

    with qtbot.waitSignal(notifier.notified, timeout=1000):  # ожидание до 1000мс
        notifier.trigger_after_delay()
    # Если сигнал НЕ сработает за отведённый timeout — тест явно и понятно завалится с ошибкой timeout,
    # а не "зависнет" неопределённо или даст ложно успешный результат от слишком раннего assert
# Тестирование C++ интеграции через PySide6-обёртку модели (статья 504)
def test_task_model_add_task(qtbot):
    model = TaskListModel()
    assert model.rowCount() == 0

    model.addTask("Тестовая задача")

    assert model.rowCount() == 1
    index = model.index(0, 0)
    assert model.data(index, TaskListModel.TitleRole) == "Тестовая задача"

Пояснения к коду

qtbot.addWidget(button) — практичный паттерн, аналогичный createTemporaryObject из Qt Quick Test (статья 496) — регистрация виджета у qtbot обеспечивает его автоматическое, корректное закрытие/очистку после завершения теста, без необходимости явного, отдельного кода очистки в каждом тесте. qtbot.waitSignal(notifier.notified, timeout=1000) показывает ключевую возможность, решающую частую проблему тестирования асинхронного кода — без подобного механизма тест, выполняющий немедленную проверку сразу после trigger_after_delay(), завершился бы неудачей просто потому, что сигнал ещё не успел сработать (поскольку он сработает только через 200мс), и waitSignal корректно приостанавливает выполнение теста, ожидая срабатывания сигнала (или истечения тайм-аута, что даёт явную, понятную ошибку вместо неопределённого зависания теста). Тест test_task_model_add_task показывает прямую проверку модели (статья 504) без какого-либо реального визуального виджета — демонстрируя, что значительная часть логики PySide6-приложения (особенно модели данных) может и должна тестироваться непосредственно, в изоляции от пользовательского интерфейса, аналогично общему принципу разделения логики и представления, подчёркиваемому во всём плане статей.

Подводные камни

  • Отсутствие qtbot.addWidget() для созданных в тестах виджетов, что может привести к накоплению не до конца очищенных виджетов между разными тестами (аналогично подводному камню изоляции тестов из статьи 439 SQL-цикла и статьи 496 QML-цикла, но применительно к Qt Widgets) — без явной регистрации виджета у qtbot ответственность за его корректную очистку полностью переходит на сам тестовый код, что легко упустить и что может привести к трудно диагностируемым взаимным влияниям между формально независимыми тестами.
  • Использование простого time.sleep() вместо qtbot.waitSignal()/qtbot.wait() для ожидания асинхронных событий — простое, фиксированное по времени ожидание либо неоправданно замедляет тесты (если реальное событие происходит значительно быстрее, чем выбранная длительность сна), либо ненадёжно (если событие иногда происходит медленнее ожидаемого фиксированного времени сна, что особенно вероятно в CI-средах с переменной, не гарантированной производительностью, статья 449 DevOps-цикла) — waitSignal/специализированные методы qtbot корректно адаптируются к реальному времени срабатывания события, ожидая ровно столько, сколько нужно, в пределах явно заданного тайм-аута.
  • Тестирование сложной бизнес-логики только через полный путь взаимодействия с пользовательским интерфейсом (клики, заполнение полей через qtbot), вместо прямого, более быстрого и изолированного тестирования соответствующей логики/моделей напрямую (как test_task_model_add_task) — аналогично общему принципу архитектурного разделения логики и представления, тесты через полный UI-путь полезны для проверки именно интеграции и пользовательского взаимодействия, но не должны быть единственным способом тестирования всей логики приложения, которая, при правильном архитектурном разделении, может и должна тестироваться более прямо и эффективно.
  • Игнорирование интеграции pytest-qt тестов в общий CI-пайплайн проекта (статья 449 DevOps-цикла), аналогично подводному камню статьи 496 QML-цикла про Qt Quick Test — pytest-qt тесты запускаются обычным pytest, что делает их интеграцию в CI относительно простой (в отличие от Qt Quick Test, требующего специфичной интеграции с CMake/ctest), и эта простота интеграции не должна стать причиной пропуска самого шага явного включения этих тестов в автоматизированный, регулярно запускаемый процесс проверки кода.