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
tests/bdd/features/に.feature(Japanese Gherkin is allowed)tests/bdd/steps/にtest_<feature名>.pycreatescenarios()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))
@given/@when/@thenimplement 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_fixturethe 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 areparsers.cfparse。 bddmarkers are added automatically:tests/bdd/conftest.pyのpytest_collection_modifyitemsadds them. Do not write@pytest.mark.bddby hand.- Data-driven is
Scenario Outline+Examples: a table directly under a step is received via thedatatableargument. - Detection of unimplemented steps:
uv run pytest --generate-missing --feature tests/bdd/features tests/bdd/steps/