# Migrate a pytest Hardware Suite to OpenHTF
URL: /guides/migrate-from-pytest
Convert a pytest suite that drives hardware into OpenHTF — test functions to phases, fixtures to plugs, assert ranges to measurements with limits, parametrize to with_args, markers to run_if, and conftest options to configuration — with a mapping table and worked example.
pytest suites that talk to hardware are common: fixtures open instruments, `test_*` functions assert on readings. They run fine on a bench and fall short on a line — no per-unit record, no measured values, no operator flow. The good news: the structure maps almost one-to-one onto OpenHTF.
pytest
OpenHTF
Notes
`test_*` function
Phase
Same body, decorated; return value controls flow instead of raising
`assert lo <= x <= hi, "label"`
`@htf.measures(htf.Measurement("label").in_range(lo, hi))` + `test.measurements.label = x`
Value, limits and unit now stored
`assert x == expected`
`.equals(expected)`
`assert re.match(p, s)`
`.matches_regex(p)`
Fixture (function scope)
Plug
One instance per run; `tearDown` replaces the fixture's `yield` cleanup
Fixture (session scope)
Plug + module-level state, or config
Plugs live for one run; long-lived connections can be cached at module level
`conftest.py` options / `pytest.ini`
Configuration (`CONF.declare`, YAML, `--config-value`)
`@pytest.mark.parametrize`
`phase.with_args(...)` / `with_plugs(...)`
See the with\_plugs example
`@pytest.mark.skipif(cond)`
`@htf.PhaseOptions(run_if=lambda: not cond)`
`run_if` leaves no record; `PhaseResult.SKIP` records a skip
`pytest.fail()` / raising
`return htf.PhaseResult.STOP` (or `FAIL_AND_CONTINUE`)
Raising still works: outcome `ERROR`; add types to `failure_exceptions` to make it `FAIL`
`@pytest.mark.flaky(reruns=3)`
`@htf.PhaseOptions(repeat_limit=3)` + `return PhaseResult.REPEAT`
Test ordering plugins
Argument order to `htf.Test(...)`, phase groups
Ordering is native
`input()` in a fixture
`UserInput.prompt()` / `prompt_for_test_start()`
Also renders in the Operator UI
`--junitxml`
`OutputToJSON` and other callbacks
Per-unit record instead of per-run XML
**All phases run by default.** pytest stops a test function at the first failed assert; OpenHTF records the failed measurement and continues to the next phase. Use `stop_on_first_failure` or checkpoints to stop early.
**One DUT per execution.** `test.execute()` is one unit. A station loops; a bench script runs once.
**Exceptions are `ERROR`, not `FAIL`.** Assertions that raised in pytest become measurements; genuine exceptions mean a broken test unless listed in `failure_exceptions` (Test Options).
**Fixture scopes collapse to "per run".** Expensive connections that should outlive a run can be opened lazily at module level and wrapped by a plug that does not close them.
Unit-test the plugs with pytest and a mocked `pyvisa` — the instrument classes are now plain Python, easy to test. See OpenHTF vs pytest for the division of labour.
If the suite must stay pytest, TofuPilot runs pytest suites on stations and promotes `assert lo <= x <= hi, "label"` to measurements with limits — Pytest on TofuPilot. You get per-unit records without a rewrite, minus OpenHTF's plugs, operator prompts and flow control.