pytest integration¶
qtest ships as a real pytest plugin. Installing qtest is the
only thing you need to do — the plugin auto-registers via its
pytest11 entry point, three CLI flags appear under pytest --help,
and the bundled circuit fixtures become available once you enable
them in your conftest.py.
CLI flags¶
The plugin exposes three knobs that apply to the entire test session:
Flag |
Default |
Effect |
|---|---|---|
|
1024 |
Default shot count for the assertion-internal backend run. |
|
0.05 |
Default total-variation tolerance for
|
|
System entropy |
Master seed for the backend RNG; identical seeds → identical sample sequences. |
A typical CI invocation:
pytest -v \
--qtest-shots 8192 \
--qtest-tolerance 0.02 \
--qtest-seed 12345
You can also set those values once-per-project in pyproject.toml:
[tool.qtest]
shots = 4096
tolerance = 0.03
seed = 42
The plugin reads [tool.qtest] at session start and CLI flags
override file settings.
Built-in circuit fixtures¶
The fixtures live in two modules — qtest.fixtures.common_states
for state-preparation circuits and qtest.fixtures.common_gates for
gate-layer factories. They are plain pytest plugins, so you enable
them by listing the modules under pytest_plugins in your top-level
conftest.py:
# tests/conftest.py
pytest_plugins = [
"qtest.fixtures.common_states",
"qtest.fixtures.common_gates",
]
Once that one line is in place, every test in the suite can pull in any of these fixtures by simply naming them as a parameter:
Fixture |
Yields |
|---|---|
|
A 2-qubit Bell-state preparation circuit. |
|
A 1-qubit \(|+\rangle\) preparation circuit. |
|
A 1-qubit \(|-\rangle\) preparation circuit. |
|
Factory |
|
Shortcuts for 3-, 4-, and 5-qubit GHZ circuits. |
|
Factory |
|
Factory |
|
Factory yielding a seeded random Clifford circuit. |
Using the fixtures together¶
from qtest import assert_distribution_close
def test_bell_is_balanced(bell_state):
# bell_state already has H + CNOT applied; we just need to measure.
qc = bell_state.copy()
qc.measure_all()
assert_distribution_close(
qc,
expected={"00": 0.5, "11": 0.5},
shots=4096,
tolerance=0.03,
)
def test_ghz_is_balanced(ghz_5):
qc = ghz_5.copy()
qc.measure_all()
assert_distribution_close(
qc,
expected={"00000": 0.5, "11111": 0.5},
shots=4096,
tolerance=0.03,
)
def test_random_clifford_is_unitary(random_clifford):
from qtest import assert_unitary
qc = random_clifford(num_qubits=4, depth=10, seed=42)
assert_unitary(qc, tolerance=1e-9)
Markers¶
qtest registers two pytest markers you can use to gate tests:
import pytest
@pytest.mark.slow
def test_high_shot_property(): ...
@pytest.mark.hardware
def test_real_qpu(): ...
Run only the fast tests during local development:
pytest -m "not slow"
Or filter out hardware tests when you’re offline:
pytest -m "not hardware"
Reproducibility¶
The single most useful thing the plugin does for you is make runs
reproducible. When --qtest-seed is set, every assertion that
needs randomness (the default backend’s RNG, the sampling-based
modes of assert_circuit_equivalent()) is seeded from
the same root. The net effect: a passing run on your laptop is
bit-for-bit identical to the CI run, so flakes are real flakes,
not seed drift.
CI configuration recipes¶
A pragmatic split:
# .github/workflows/ci.yml — excerpt
- name: Fast tests
run: pytest -m "not slow" --qtest-shots 1024 --qtest-tolerance 0.05
- name: Slow tests
run: pytest -m "slow" --qtest-shots 8192 --qtest-tolerance 0.02
if: github.event_name == 'schedule'
PR runs stay snappy; the nightly build runs the precise, shot-heavy properties.
Where to go next¶
Writing assertions — the assertions these fixtures feed into.
Property-based testing — how the slow markers and high shot counts play with Hypothesis profiles.
Fixtures — the full autogenerated reference for every bundled fixture.