SuperAPI AI Directory

pytest-qt-bdd

Testing & quality

View repository

Updated 10-15 21:03

About this skill

pytest-qt / pytest-bdd Gotchas

This summarizes only the points that are particularly easy to stumble on with this stack in Qt / PySide6 GUI testing and pytest-bdd behavior specification testing. The coverage policy, CI-equivalent filter, and the line for what to mock are covered by rules/testing.md , so they are not repeated here.

pytest-qt

Wait for a signal or condition, not a fixed time

qtbot.wait(ms) hard-coded waits easily flake under CI load. Wait for completion via a signal, and wait for state via a condition.

# 正しい: waitSignal でタイムアウト付き待機
with qtbot.waitSignal(widget.completed, timeout=5000):
    widget.start_operation()

# 正しい: waitUntil で条件待機
qtbot.waitUntil(lambda: widget.isEnabled(), timeout=5000)
Prohibited: fixed-time waits

qtbot.wait(1000)

Prohibited: calling processEvents directly (manually trying to guess how to run the event loop)

QCoreApplication.processEvents()


### モーダルダイアログは必ずモックする

`QMessageBox.question` 等はモーダルで止まり、headless ではテストがハングする。静的メソッドを monkeypatch で差し替える。

```python
def test_delete_confirmation(qtbot, monkeypatch):
    monkeypatch.setattr(
        QMessageBox, "question",
        lambda *args: QMessageBox.Yes
    )
    widget.delete_item()

Do not mock Signals/Slots

Qt Signal/Slot wiring is itself the subject of test verification. If you crush the connection with a mock, you cannot detect wiring mistakes. Actually emit it and receive it withwaitSignal .

headless execution

On Linux / containers, QT_QPA_PLATFORM=offscreen and launch. On Windows, this is unnecessary because it can run with native windows.

# Linux/コンテナ
QT_QPA_PLATFORM=offscreen uv run pytest -m gui

# Windows
uv run pytest -m gui

gui marker is used to distinguish "accesses Qt but display is unnecessary" fromgui_show , which means "involves actual rendering".gui_show is the target to exclude from headless CI.

pytest-bdd

BDD is not limited to E2E; apply it to the Service layer and above as a "representation format for behavior specifications".

Applicable layers

Layer Application of BDD Reason
User-facing feature flows ◎ The specification itself
Service layer behavior ○ Suited to expressing business rules
Repository layer CRUD △ Too technical and Gherkin becomes verbose
Internal logic and utilities ✕ Ordinary pytest is appropriate

Write: New user-facing features, Service layer business rules (deduplication, validation, etc.), regression prevention for bug fixes (write the reproduction scenario first, then fix). Do not write: Internal refactoring, UI appearance adjustments, simple CRUD in the Repository layer.

Procedure for adding feature / steps

  1. tests/bdd/features/ に .feature (Japanese Gherkin is allowed)
  2. tests/bdd/steps/ に test_<feature名>.py create
  3. scenarios() register features all at once
from pathlib import Path
from pytest_bdd import scenarios

_FEATURE_FILE = Path(__file__).parent.parent / "features" / "<feature名>.feature"
scenarios(str(_FEATURE_FILE))
  1. @given / @when / @then implement step definitions

Pitfalls

  • scenarios() use bulk registration: Do not write individual @scenario() decorators. Every time you add a feature, registration omissions occur.
  • The feature path is __file__ resolve absolutely based on: Relative paths break depending on pytest's launch directory.
  • State between steps is passed via target_fixture the return value of: @given(..., target_fixture="ctx") becomes a fixture and is passed to subsequent steps. Sharing via module global variables becomes a hotbed of cross-test contamination.
  • Reuse existing pytest fixtures: Inject DB managers, etc., via the arguments of conftest.py の fixture を given / when / then .
  • Keep step implementations thin: Only call Service / Repository, and do not write business logic in steps.
  • Argument parsing is parsers.parse: {name:d} with type conversion. Only when a regular expression is needed, parsers.re, and custom types are parsers.cfparse。
  • bdd markers are added automatically: tests/bdd/conftest.py の pytest_collection_modifyitems adds them. Do not write @pytest.mark.bdd by hand.
  • Data-driven is Scenario Outline + Examples: a table directly under a step is received via the datatable argument.
  • Detection of unimplemented steps: uv run pytest --generate-missing --feature tests/bdd/features tests/bdd/steps/