Skip to content
Getting Started

Use pytest for Hardware Testing

Learn how to use pytest to automate hardware tests and upload results to TofuPilot for centralized tracking and analytics.

JJulien Buteau
beginner10 min readMarch 14, 2026

How to Use pytest for Hardware Testing with TofuPilot

pytest is the most popular Python testing framework. You already know it from software testing. You can use it for hardware testing too. This guide shows how to structure hardware tests with pytest and upload results to TofuPilot.

Why pytest for Hardware

FeatureHow it helps hardware testing
FixturesSet up instruments, power supplies, connections
ParametrizeRun the same test across multiple DUTs or configurations
MarkersTag tests by type (functional, safety, calibration)
AssertionsDefine pass/fail criteria for measurements
PluginsExtend with custom reporting, parallel execution

pytest gives you the test runner. TofuPilot gives you the data platform. Together they replace ad-hoc scripts and Excel files.

Basic Hardware Test with pytest

test_power_rails.py
import pytestimport pyvisafrom tofupilot import TofuPilotClient# Fixtures for instrument setup@pytest.fixture(scope="session")def rm():    return pyvisa.ResourceManager()@pytest.fixture(scope="session")def dmm(rm):    inst = rm.open_resource("TCPIP::192.168.1.10::INSTR")    yield inst    inst.close()@pytest.fixture(scope="session")def psu(rm):    inst = rm.open_resource("TCPIP::192.168.1.11::INSTR")    inst.write("OUTP ON")    yield inst    inst.write("OUTP OFF")    inst.close()@pytest.fixture(scope="session")def tofupilot():    return TofuPilotClient()# Testsdef test_vcc_3v3(dmm, tofupilot):    voltage = float(dmm.query("MEAS:VOLT:DC?"))    assert 3.25 <= voltage <= 3.35, f"3.3V rail out of spec: {voltage}V"def test_vcc_1v8(dmm, tofupilot):    voltage = float(dmm.query("MEAS:VOLT:DC?"))    assert 1.75 <= voltage <= 1.85, f"1.8V rail out of spec: {voltage}V"def test_idle_current(dmm, tofupilot):    current = float(dmm.query("MEAS:CURR:DC?")) * 1000  # mA    assert 30 <= current <= 60, f"Idle current out of spec: {current}mA"

Uploading pytest Results to TofuPilot

Collect measurements during tests and upload at the end.

conftest.py
import pytestfrom tofupilot import TofuPilotClientclass TestCollector:    """Collects measurements during a test session."""    def __init__(self):        self.measurements = []        self.steps = []        self.all_passed = True    def add_measurement(self, step_name, name, value, unit, limit_low=None, limit_high=None):        passed = True        if limit_low is not None and value < limit_low:            passed = False        if limit_high is not None and value > limit_high:            passed = False        if not passed:            self.all_passed = False        # Find or create step        step = next((s for s in self.steps if s["name"] == step_name), None)        if not step:            step = {"name": step_name, "step_type": "measurement", "status": True, "measurements": []}            self.steps.append(step)        measurement = {"name": name, "value": value, "unit": unit}        if limit_low is not None:            measurement["limit_low"] = limit_low        if limit_high is not None:            measurement["limit_high"] = limit_high        step["measurements"].append(measurement)        if not passed:            step["status"] = False@pytest.fixture(scope="session")def collector():    return TestCollector()@pytest.fixture(scope="session", autouse=True)def upload_results(collector):    yield  # Run all tests    # Upload after all tests complete    client = TofuPilotClient()    serial = input("Enter DUT serial: ") if not hasattr(collector, "serial") else collector.serial    client.create_run(        procedure_id="PYTEST-BOARD-FUNCTIONAL",        unit_under_test={"serial_number": serial},        run_passed=collector.all_passed,        steps=collector.steps,    )
test_board.py
def test_vcc_3v3(dmm, collector):    voltage = float(dmm.query("MEAS:VOLT:DC?"))    collector.add_measurement("Power Rails", "vcc_3v3", voltage, "V", limit_low=3.25, limit_high=3.35)    assert 3.25 <= voltage <= 3.35def test_vcc_1v8(dmm, collector):    voltage = float(dmm.query("MEAS:VOLT:DC?"))    collector.add_measurement("Power Rails", "vcc_1v8", voltage, "V", limit_low=1.75, limit_high=1.85)    assert 1.75 <= voltage <= 1.85def test_idle_current(dmm, collector):    current = float(dmm.query("MEAS:CURR:DC?")) * 1000    collector.add_measurement("Current Draw", "idle_current_ma", current, "mA", limit_low=30, limit_high=60)    assert 30 <= current <= 60

Option 2: pytest Plugin

Write a pytest plugin that hooks into test results.

conftest.py
import pytestfrom tofupilot import TofuPilotClientdef pytest_sessionfinish(session, exitstatus):    """Upload results after all tests complete."""    client = TofuPilotClient()    passed = exitstatus == 0    # Collect results from the session    steps = []    for item in session.items:        report = item.stash.get("report", None)        if report:            steps.append({                "name": item.name,                "step_type": "measurement",                "status": report.passed,                "measurements": item.stash.get("measurements", []),            })    client.create_run(        procedure_id="PYTEST-FUNCTIONAL",        unit_under_test={"serial_number": session.config.getoption("--serial", "UNKNOWN")},        run_passed=passed,        steps=steps,    )

Running Tests

terminal
# Run all hardware testspytest test_board.py -v# Run with serial numberpytest test_board.py --serial UNIT-5501# Run only power rail testspytest test_board.py -k "vcc" -v# Run with markerspytest test_board.py -m "safety" -v

Organizing Hardware Tests with pytest

Use Markers for Test Categories

test_board.py
import pytest@pytest.mark.safetydef test_hipot(hipot_tester, collector):    """Safety test - must pass for every unit."""    leakage = hipot_tester.run_test(1500)    collector.add_measurement("Safety", "hipot_leakage_ma", leakage, "mA", limit_high=5.0)    assert leakage < 5.0@pytest.mark.functionaldef test_communication(dut, collector):    """Functional test - verifies basic operation."""    response = dut.ping()    collector.add_measurement("Communication", "uart_ping", 1 if response else 0, "bool", limit_low=1)    assert response@pytest.mark.calibrationdef test_adc_accuracy(dut, collector):    """Calibration test - verifies measurement accuracy."""    error = dut.measure_adc_error()    collector.add_measurement("Calibration", "adc_error_pct", error, "%", limit_high=0.5)    assert error < 0.5

Use Parametrize for Multi-Channel Tests

test_multi_channel.py
import pytest@pytest.mark.parametrize("channel,expected_v,tolerance", [    (1, 3.3, 0.05),    (2, 1.8, 0.05),    (3, 5.0, 0.10),    (4, 12.0, 0.20),])def test_voltage_rail(dmm, collector, channel, expected_v, tolerance):    voltage = dmm.measure_channel(channel)    collector.add_measurement(        "Power Rails",        f"rail_ch{channel}_v",        voltage,        "V",        limit_low=expected_v - tolerance,        limit_high=expected_v + tolerance,    )    assert abs(voltage - expected_v) < tolerance

pytest vs. OpenHTF

FeaturepytestOpenHTF
Learning curveLow (most Python devs know it)Medium (hardware-specific)
Hardware test featuresGeneral-purposePurpose-built (phases, plugs, measurements)
Operator UINone built-inBuilt-in web UI
CommunityMassiveSmall but focused
TofuPilot integrationVia custom codeNative callback

Use pytest when your team already knows it and you want something running quickly. Use OpenHTF when you need the full hardware test framework with operator interface and structured phases.

Both upload to TofuPilot the same way. The data in TofuPilot looks identical regardless of which framework generated it.

More Guides

Put this guide into practice