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
| Feature | How it helps hardware testing |
|---|---|
| Fixtures | Set up instruments, power supplies, connections |
| Parametrize | Run the same test across multiple DUTs or configurations |
| Markers | Tag tests by type (functional, safety, calibration) |
| Assertions | Define pass/fail criteria for measurements |
| Plugins | Extend 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
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
Option 1: Upload in a Fixture (Recommended)
Collect measurements during tests and upload at the end.
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, )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 <= 60Option 2: pytest Plugin
Write a pytest plugin that hooks into test results.
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
# 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" -vOrganizing Hardware Tests with pytest
Use Markers for Test Categories
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.5Use Parametrize for Multi-Channel Tests
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) < tolerancepytest vs. OpenHTF
| Feature | pytest | OpenHTF |
|---|---|---|
| Learning curve | Low (most Python devs know it) | Medium (hardware-specific) |
| Hardware test features | General-purpose | Purpose-built (phases, plugs, measurements) |
| Operator UI | None built-in | Built-in web UI |
| Community | Massive | Small but focused |
| TofuPilot integration | Via custom code | Native 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.