By the end of this page, you'll have a working pytest-f3ts test that talks to real hardware. Python is already the dominant language for hardware testing — every major instrument manufacturer ships Python drivers, and pytest gives you discovery, fixtures, assertions, and reporting out of the box. pytest-f3ts adds what's missing: instrument control, measurement logging, and pass/fail criteria for automated PCB testing on your DUT.
Prerequisites#
- Python 3.9+ installed
- pytest-f3ts installed — see Installation if you haven't done this yet
- A connected DUT with at least one measurable test point, or a simulated target for dry-run testing
You don't need a full test plan or YAML configuration to run your first test. Those come later in Configuration and Test Plans.
Writing Your First Hardware Test#
A hardware test follows the same pattern as any pytest test: set up the hardware state, take a measurement, assert against a limit. pytest-f3ts gives you fixtures that manage instrument connections so you don't write that boilerplate yourself.
The hardware interface#
Create a class that wraps your instruments. This example uses a power supply and a digital multimeter:
"""hardware_interface.py"""
from power_supply import Supply
from multimeter import Multimeter
class Interface:
pinmap = {"5V": 7, "3V3": 3, "1V1": 5}
def __init__(self):
self.supply = Supply()
self.multimeter = Multimeter()
def dut_en_5v(self, state: bool):
self.supply.set_voltage(state, voltage=5.0)
def check_supply_shorted(self):
return self.supply.get_voltage() < 4.0
def get_voltage(self, test_point: str) -> float:
channel = self.pinmap[test_point]
return self.multimeter.get_voltage(channel=channel)
def close(self):
self.supply.close()
self.multimeter.close()Replace Supply and Multimeter with your actual instrument drivers. The interface class is the only place that knows how to talk to your specific hardware — everything else works through it.
The pytest fixture#
Wire the interface into pytest's fixture system via conftest.py:
"""conftest.py"""
import pytest
from hardware_interface import Interface
@pytest.fixture(scope="session")
def hardware_interface():
interface = Interface()
yield interface
interface.close()The session scope means pytest creates one interface instance for the entire test run and tears it down when all tests finish. No manual cleanup required.
The test#
"""test_voltages.py"""
def test_dut_en_5v(hardware_interface):
"""Verify 5V supply is not shorted."""
hardware_interface.dut_en_5v(True)
shorted = hardware_interface.check_supply_shorted()
hardware_interface.dut_en_5v(False)
assert not shorted, "5V supply shorted to GND"
def test_3v3_voltage(hardware_interface):
"""Verify 3.3V rail is within tolerance."""
hardware_interface.dut_en_5v(True)
voltage = hardware_interface.get_voltage(test_point="3V3")
hardware_interface.dut_en_5v(False)
assert 3.2 < voltage < 3.4, f"3V3 rail measured {voltage}V"That's a complete, runnable test. Three files: the hardware interface, the conftest fixture, and the test itself.
Running Your Tests#
pytest -sThe -s flag shows print output from your fixtures, which is useful when you're debugging hardware setup and teardown. You'll see output like this:
============================= test session starts =============================
collected 2 items
test_voltages.py::test_dut_en_5v PASSED
test_voltages.py::test_3v3_voltage PASSED
============================== 2 passed in 1.23s ==============================
Common first-run issues#
ModuleNotFoundError for your instrument driver. Your instrument library isn't installed or isn't on the Python path. Check your virtual environment.
Tests hang during setup. The instrument connection is timing out. Verify your DUT is powered and connected. Check the instrument address (GPIB, USB, TCP) in your interface class.
All tests fail with the same error. If the session-scoped fixture fails, every test that depends on it fails too. Fix the fixture first — the individual test failures are downstream.
Understanding Test Results#
Each test produces a pass or fail based on its assertions. For hardware tests, the assertion is typically a measurement compared against limits: assert 3.2 < voltage < 3.4.
When a test fails, the assertion message tells you what went wrong:
FAILED test_voltages.py::test_3v3_voltage - AssertionError: 3V3 rail measured 2.91V
That measured value is the diagnostic. It tells your test engineer whether the board has a component issue, a solder defect, or a design problem.
Logging measurements with pytest-f3ts#
For production testing, you'll want measurement data recorded beyond just pass/fail. pytest-f3ts recognizes standard variables — meas, min_limit, max_limit, error_code, error_msg — and logs them to JUnitXML, JSON, and CSV formats automatically.
The quickest way to add logging:
from pytest_f3ts.utils import log_vars
def test_3v3_voltage(hardware_interface, record_property):
min_limit = 3.2
max_limit = 3.4
meas = hardware_interface.get_voltage(test_point="3V3")
log_vars(record_property)
assert min_limit < meas < max_limit, f"3V3 rail: {meas}V"log_vars inspects local variables and automatically records any that match standard names (meas, min_limit, max_limit, etc.) to the test log. No manual record_property calls needed for each variable.
For tests with multiple measurements, use the f3ts_assert fixture to report sub-assertions individually. See the API Reference for details.
Next Steps#
- API Reference — Full documentation of fixtures, utilities, and configuration options
- Configuration — Set up YAML config files for test limits and metadata
- Examples — Patterns for voltage testing, continuity checks, LED verification, and more
- Test Plans — Structured test plans with IDs, error codes, and limit management