pytest-qt Documentation

repository·master·Indexed 19 days ago

https://github.com/pytest-dev/pytest-qt

A pytest plugin for testing applications built with PyQt5, PyQt6, or PySide6. It provides the qtbot fixture for simulating user interactions and managing application lifecycles, tools for capturing Qt logging messages (qDebug, qWarning, qCritical), and the qtmodeltester fixture for verifying QAbstractItemModel implementations. Features include signal waiting, automatic exception handling in slots, and utilities for capturing widget screenshots.

Tokens
14.7K
Snippets
55
Records
69
Agent score
66%

What's inside pytest-qt

  1. Overview of pytest-qt features

    master

    pytest-qt provides several specialized tools for testing Qt-based applications:

    • qtbot fixture: Simulates user interactions with Qt widgets.
    • Automatic capture: Automatically captures qDebug, qWarning, and qCritical messages.
    • Signal waiting: Provides waitSignal and waitSignals functions to block test execution until specific signals are emitted.
    • Exception handling: Automatically captures exceptions occurring in virtual methods and slots, failing the test accordingly.
  2. Key features of pytest-qt

    master
    • qtbot fixture: Simulates user interaction with Qt widgets.
    • Automatic capture: Automatically captures qDebug, qWarning, and qCritical messages.
    • Signal waiting: Provides waitSignal and waitSignals functions to block test execution until specific signals are emitted.
    • Exception handling: Automatically captures exceptions in virtual methods and slots, causing tests to fail accordingly.
  3. Capture Qt logging messages

    master

    By default, pytest-qt captures Qt's internal logging (via qInstallMessageHandler, qDebug, qWarning, and qCritical) and displays them in the test failure report, similar to how pytest captures stdout and stderr. This allows you to see internal Qt warnings or errors when an assertion fails.

    from pytestqt.qt_compat import qt_api
    
    def do_something():
        qt_api.qWarning("this is a WARNING message")
    
    def test_foo():
        do_something()
        assert 0
  4. How pytest-qt handles exceptions in virtual methods

    master

    In Qt, overriding virtual C++ methods (like mouseReleaseEvent) is common. By default, many Qt bindings (like PyQt5 and PyQt6) do not raise Python exceptions at the calling point when an error occurs inside these methods; instead, they may print a stack trace or even crash the interpreter via abort().

    pytest-qt automatically installs an exception hook to make testing more predictable. This hook captures errors raised inside virtual methods and fails the test with a clear error message, ensuring that unexpected behavior in the Qt event loop is caught during your test suite.

    class MyWidget(QWidget):
        def mouseReleaseEvent(self, ev):
            raise RuntimeError("unexpected error")
    
    # pytest-qt will capture this RuntimeError and fail the test
    w = MyWidget()
    QTest.mouseClick(w, QtCore.Qt.LeftButton)
  5. Interacting with widgets: Widget methods vs. qtbot simulation

    master

    When writing tests, you have two ways to interact with widgets. Choosing the right one is critical for test reliability:

    1. Prefer Widget Methods (Recommended): Use the widget's own API (e.g., QLineEdit.setText(), QComboBox.setCurrentIndex()) to change state. These methods emit the appropriate signals, ensuring the application behaves exactly as if a user had interacted with it, without the overhead of event loop processing.

    2. Use qtbot Simulation (Specialized cases): Use qtbot methods like keyClicks or mouseClick only when testing custom drawing widgets or specific low-level event handling.

    Warning: Using qtbot to simulate raw clicks/keys for standard controls (like selecting a combo box item) can make tests flaky because these actions trigger events that must be processed in the next pass of the event loop. It is also more complex to implement (e.g., a combo box selection might require multiple clicks to simulate correctly).

  6. Take screenshots of widgets with QtBot.screenshot

    master

    You can capture a screenshot of a specific widget using qtbot.screenshot(widget, suffix=None). The screenshot is saved to a temporary directory managed by pytest (typically /tmp/pytest-of-USER/pytest-N/...).

    Filename Generation Logic: The filename is constructed using the following components:

    1. The literal string screenshot
    2. The class name of the widget (e.g., QPushButton)
    3. The widget's objectName(), if it has been set
    4. An optional suffix if provided to the method
    5. A unique counter to prevent collisions if multiple screenshots are taken

    To use this for debugging, it is a common pattern to intentionally fail the test and assert against the returned path so the file location is printed in the test output.

    from pytestqt.qt_compat import qt_api
    
    def test_screenshot(qtbot):
        button = qt_api.QtWidgets.QPushButton()
        button.setText("Hello World!")
        qtbot.add_widget(button)
        
        # Capture the screenshot and get the file path
        path = qtbot.screenshot(button)
        
        # Fail the test to print the path to the console
        assert False, path
  7. Stop the current test using QtBot.stop

    master

    If a GUI test is failing or stuck, you can interrupt the current test by calling qtbot.stop(). This method closes all visible windows and attempts to restore the previous state before allowing the test to continue running.

    Note on Headless Environments: If you are running tests in a headless environment using Xvfb or the offscreen platform plugin (e.g., via QT_QPA_PLATFORM=offscreen), you will not be able to see the windows. To see the windows during debugging, disable these tools. If you use the pytest-xvfb plugin, you can disable it by passing the --no-xvfb flag to pytest.

    # Example usage within a test
    def test_something(qtbot):
        # ... setup code ...
        if some_error_condition:
            qtbot.stop()
  8. Test QAbstractItemModel implementations with qtmodeltester

    master

    The qtmodeltester fixture provides a way to continuously verify QAbstractItemModel implementations. It monitors the model as it changes to catch common errors such as incorrect row counts, off-by-one bugs, inconsistent index() calls, and mismatches between hasChildren() and rowCount().

    To use it, pass the qtmodeltester fixture to your test function and call qtmodeltester.check(model). If the tester detects an inconsistency or an error, the test will fail with an assertion pinpointing the issue.

    def test_standard_item_model(qtmodeltester):
        model = QStandardItemModel()
        items = [QStandardItem(str(i)) for i in range(4)]
        model.setItem(0, 0, items[0])
        model.setItem(0, 1, items[1])
        model.setItem(1, 0, items[2])
        model.setItem(1, 1, items[3])
        qtmodeltester.check(model)
  9. Disable automatic exception capture in virtual methods

    master

    If you need to disable the pytest-qt exception hook (for example, if you are implementing your own custom exception handling), you can do so at the test level or the project level.

    Note on Compatibility:

    • PySide6 6.5.2+: This option has no effect because PySide6 handles these exceptions natively.
    • PyQt5.5+ and PyQt6: Disabling this is not recommended unless you have installed your own exception hook, as exceptions in these versions can trigger abort() and crash the Python interpreter.
    ### Disable for a single test
    @pytest.mark.qt_no_exception_capture
    def test_buttons(qtbot):
        ...
    
    ### Disable for the entire project (pytest.ini)
    [pytest]
    qt_no_exception_capture = 1
  10. Disable Qt logging capture

    master

    You can disable Qt log capturing using the following methods:

    1. CLI Flag: Pass --no-qt-log to the pytest command. This falls back to default Qt behavior where messages are printed directly to stderr.
    2. Pytest Capture: Using the standard -s or --capture=no option will also disable Qt log capturing.

    When disabled, captured messages will not appear in the pytest-qt failure summary.

    pytest test.py -q --no-qt-log
  11. Avoid event loop failure when testing QApplication.exit()

    master

    Calling QApplication.exit() during a test will terminate the main event loop and all auxiliary event loops. This causes subsequent pytest-qt features that rely on the event loop, such as waitSignal and waitSignals, to fail.

    To test that an application correctly triggers an exit without actually stopping the event loop, you should monkeypatch or mock QApplication.exit() to intercept the call.

    def test_exit_button(qtbot, monkeypatch):
        exit_calls = []
        # Intercept the exit call so the event loop keeps running
        monkeypatch.setattr(QApplication, "exit", lambda: exit_calls.append(1))
        
        button = get_app_exit_button()
        button.click()
        
        assert exit_calls == [1]
  12. Configure CI pipelines (GitHub Actions, GitLab, Azure) for Qt testing

    master

    Running pytest-qt in CI environments requires installing specific X11 and Qt dependencies and starting an Xvfb server.

    Common Dependencies

    For most Linux runners, you will need to install:

    • libxkbcommon-x11-0
    • libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, libxcb-render-util0, libxcb-xinerama0, libxcb-xfixes0
    • x11-utils
    • libgl1, libegl1, libdbus-1-3

    Note for Qt6: You must also install xcb-cursor0 (version 1.11+ on the runner).