Four frameworks dominate manufacturing test automation: OpenHTF, pytest, OpenTAP, and NI TestStand. Each makes different tradeoffs between structure, flexibility, cost, and ecosystem. This guide compares them with real code, concrete metrics, and decision criteria so you can pick the right one.
Framework Overview
| Framework | Language | License | Origin | Focus |
|---|---|---|---|---|
| OpenHTF | Python | Apache 2.0 (free) | Manufacturing/production test | |
| pytest | Python | MIT (free) | Community | Software testing, adapted for hardware |
| OpenTAP | C# / Python | MPL 2.0 (free) | Keysight | Instrument-heavy test automation |
| NI TestStand | LabVIEW / C / Python | Commercial ($4,310/seat) | NI (Emerson) | Enterprise manufacturing test |
Feature Comparison Matrix
| Feature | OpenHTF | pytest | OpenTAP | TestStand |
|---|---|---|---|---|
| Structured measurements | Built-in (name, value, limits, units) | Manual (assert only) | Plugin-based | Built-in |
| Serial number input | Built-in prompt | Manual | Plugin | Built-in |
| Operator UI | Built-in web UI | None | Built-in (GUI editor) | Built-in (Sequence Editor) |
| Test sequencing | Phase ordering | Function ordering (plugins) | Step ordering (GUI) | Sequence files (GUI) |
| Parallel DUT | Limited | Native (pytest-xdist) | Native | Native |
| Instrument drivers | Plugs (Python) | Fixtures (Python) | Plugins (C#/Python) | NI drivers (LabVIEW) |
| Report format | Protobuf | JUnit XML | XML/JSON | XML/database |
| Version control | Git (Python files) | Git (Python files) | Git (XML + code) | Difficult (binary .seq files) |
| CI/CD integration | Native (Python) | Native (Python) | Possible | Difficult |
| Cross-platform | Linux, macOS, Windows | Linux, macOS, Windows | Linux, Windows | Windows only |
| Community | Small (~640 stars) | Massive (11K+ stars) | Small (~200 stars) | Large (NI forums) |
| Learning curve | Medium | Low | Medium | High |
| Annual cost (5 seats) | $0 | $0 | $0 | ~$21,550 |
The Same Test in Each Framework
A simple functional test: measure a 3.3V rail voltage, check it's within 3.2V to 3.4V.
OpenHTF
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
class DutPlug(htf.plugs.BasePlug):
def setUp(self):
self.voltage = 3.31 # Replace with instrument read
def tearDown(self):
pass
@htf.measures(
htf.Measurement("rail_3v3")
.in_range(3.2, 3.4)
.with_units(units.VOLT),
)
@htf.plug(dut=DutPlug)
def test_power(test, dut):
test.measurements.rail_3v3 = dut.voltage
def main():
test = htf.Test(test_power, procedure_id="FCT-001", part_number="PCBA-100")
with TofuPilot(test):
test.execute(test_start=lambda: input("Serial: "))
if __name__ == "__main__":
main()Strengths: Measurements are structured data (name, value, limits, units). One line for TofuPilot integration. Operator gets a serial number prompt automatically.
pytest
import pytest
from tofupilot import TofuPilotClient
@pytest.fixture
def dut():
connection = {"voltage": 3.31} # Replace with real connection
yield connection
def test_power_rail(dut):
voltage = dut["voltage"]
assert 3.2 <= voltage <= 3.4, f"3.3V rail: {voltage}V"Strengths: Familiar to every Python developer. Huge plugin ecosystem. Flexible fixture system. Great for R&D and validation.
Weakness: Measurements are implicit (assert statements). No structured data for analytics without extra code.
OpenTAP
// OpenTAP C# test step
using OpenTap;
[Display("Power Rail Test", Group: "FCT")]
public class PowerRailStep : TestStep
{
[Display("Lower Limit")]
public double LowerLimit { get; set; } = 3.2;
[Display("Upper Limit")]
public double UpperLimit { get; set; } = 3.4;
public override void Run()
{
double voltage = 3.31; // Replace with instrument read
Results.Publish("rail_3v3", new { Voltage = voltage });
if (voltage < LowerLimit || voltage > UpperLimit)
UpgradeVerdict(Verdict.Fail);
}
}Strengths: GUI step editor for non-programmers. Strong Keysight instrument integration. Plugin architecture for test plans.
Weakness: C# primary language (Python plugin exists but is secondary). Smaller community.
NI TestStand
TestStand uses a visual sequence editor. The equivalent test is a sequence file (.seq) with a "Numeric Limit Test" step configured via GUI: test value = voltage reading, low limit = 3.2, high limit = 3.4.
Strengths: Mature, enterprise-grade. Deep NI hardware integration. Built-in report generation, database logging, parallel execution.
Weakness: $4,310/seat/year. Windows only. Binary sequence files don't version-control well. Tied to NI ecosystem.
Cost Analysis
| OpenHTF | pytest | OpenTAP | TestStand | |
|---|---|---|---|---|
| License (5 seats) | $0 | $0 | $0 | $21,550/year |
| License (20 seats) | $0 | $0 | $0 | $86,200/year |
| Runtime deployment | Free | Free | Free | Additional runtime licenses |
| Training | Self-taught (docs) | Self-taught (docs) | Self-taught (docs) | NI training courses ($2K+) |
| Vendor lock-in | None | None | Low (Keysight-adjacent) | High (NI ecosystem) |
| Support | Community (GitHub) | Community (massive) | Community + Keysight | NI support contract |
When to Use Each Framework
| Scenario | Best Choice | Why |
|---|---|---|
| Production FCT, Python team | OpenHTF | Built for manufacturing test, structured measurements, operator UI |
| R&D validation, firmware CI | pytest | Flexible, fast iteration, CI/CD native, huge ecosystem |
| Multi-vendor instruments, enterprise | OpenTAP | Plugin architecture, GUI editor, Keysight integration |
| Existing NI hardware, large enterprise | TestStand | Deep NI integration, enterprise support, existing infrastructure |
| Small team, budget-conscious | OpenHTF or pytest | Zero license cost, Python ecosystem |
| Mixed (R&D + production) | pytest + OpenHTF | pytest for validation, OpenHTF for production |
Migration Paths
From TestStand to OpenHTF
| TestStand Concept | OpenHTF Equivalent |
|---|---|
| Sequence file (.seq) | Python test script |
| Step types | Phase functions |
| Numeric Limit Test | @htf.measures with .in_range() |
| String Value Test | @htf.measures with .equals() |
| Step module (code) | Plug class |
| Process model | TofuPilot integration |
| Report generation | TofuPilot dashboard |
| UUT serial number | test.execute(test_start=lambda: input("Serial: ")) |
From pytest to OpenHTF
| pytest Concept | OpenHTF Equivalent |
|---|---|
| Test function | Phase function |
| Fixture | Plug |
| Assert statement | @htf.measures with validators |
| conftest.py | Plug classes in shared module |
| JUnit XML | Protobuf output + TofuPilot |
TofuPilot Integration
All four frameworks work with TofuPilot:
| Framework | Integration Method | Effort |
|---|---|---|
| OpenHTF | Native (with TofuPilot(test)) | 1 line |
| pytest | Python SDK (TofuPilotClient) | ~10 lines per test |
| OpenTAP | REST API or Python SDK | Medium |
| TestStand | REST API | Medium |
Decision Flowchart
- Do you have existing TestStand infrastructure? Yes, and it works well: stay with TestStand. Yes, but you want to migrate: move to OpenHTF.
- Are you building production tests? Yes: OpenHTF. It was built for this.
- Are you doing R&D or firmware validation? Yes: pytest. More flexible, better CI/CD.
- Do you need a GUI for non-programmers? Yes: OpenTAP. Visual step editor.
- Unsure? Start with pytest. It's the easiest to learn and you can always add OpenHTF for production later.