Test Types & Methods

What Is Automated Test Equipment (ATE)

Automated test equipment combines instruments, a fixture and software to test units without manual measurement. What ATE is and how to build one.

JJulien Buteau
beginner10 min readMarch 14, 2026

Automated test equipment is the combination of instruments, a fixture and software that tests a unit without an engineer taking readings by hand. The term covers everything from a bench with a multimeter and a script to a semiconductor tester costing millions. What they share is the same three layers.

The Three Layers

LayerWhat it isExamples
InstrumentsThe things that stimulate and measureDMM, power supply, oscilloscope, DAQ, RF analyser
FixtureThe mechanical and electrical interface to the unitPogo-pin bed, clamshell, cable harness, handler
SoftwareSequencing, limits, records, operator interactionThe test executive and your test code

Most ATE problems that look like software problems are fixture problems, and most that look like fixture problems are contact problems. That ordering is worth remembering when yield drops on one station and not another.

Types of ATE

TypeTestsTypical cost
Bench ATEOne board, one operator, low volumeThousands
Functional test fixture (FCT)Assembled PCBAs at end of lineTens of thousands
In-circuit tester (ICT)Components and nets on a bare-populated boardHundreds of thousands
Flying probeSame as ICT, no dedicated fixture, slowerHundreds of thousands
Semiconductor ATEWafers and packaged dieMillions
System-level testFully assembled productsVaries

What the Software Layer Has to Do

This is the part teams underestimate, because instruments and fixtures are visible and software is not.

ResponsibilityWhy it matters
SequencingSteps run in a defined order, with dependencies
Limit evaluationPass/fail derived from the measurement, not hand-coded
Instrument lifecycleSessions opened once, shared, closed even on failure
Operator interactionSerial scan, prompts, clear pass/fail indication
Result recordsThe measured value survives the shift
TraceabilityWhich unit, which station, which limits, which code
RecoveryA failure stops safely, not halfway through energised

A test that prints PASS to a console has done the first two and none of the rest.

Building a Simple ATE

The three layers map directly onto a procedure: instruments are plugs, the sequence and limits are the procedure, and the test logic is phases.

plugs/psu.py
class PowerSupply:    """Instrument layer: programmable supply."""    def configure(self, volts: float, current_limit: float):        pass  # Replace with SCPI writes    def output(self, on: bool):        pass    def measure_current_ma(self) -> float:        return 41.3    def close(self):        self.output(False)
plugs/dmm.py
class Multimeter:    def dc_voltage(self) -> float:        return 3.31  # Replace with a PyVISA query
plugs/fixture.py
class Fixture:    """Fixture layer: interlock and contact sensing."""    def lid_closed(self) -> bool:        return True    def contact_ok(self) -> bool:        return True
phases/safety.py
def safety(measurements, fixture):    """Nothing is energised until the fixture says it is safe."""    measurements.lid_closed = fixture.lid_closed()    measurements.contact_ok = fixture.contact_ok()
phases/power_up.py
def power_up(measurements, psu, dmm):    psu.configure(volts=5.0, current_limit=0.5)    psu.output(True)    measurements.rail_3v3 = dmm.dc_voltage()    measurements.inrush_current = psu.measure_current_ma()
procedure.yaml
46 lines
name: Bench ATE Functional Testversion: 1.0.0unit:  serial_number:    default_value: "PCBA000001"  part_number:    default_value: "PCBA-100"plugs:  - name: psu    python: plugs.psu:PowerSupply  - name: dmm    python: plugs.dmm:Multimeter  - name: fixture    python: plugs.fixture:Fixturemain:  - name: Safety    python: phases.safety    measurements:      - name: Lid Closed        validators:          - operator: "=="            expected_value: true      - name: Contact Ok        validators:          - operator: "=="            expected_value: true  - name: Power Up    python: phases.power_up    depends_on: [safety]    measurements:      - name: Rail 3V3        unit: V        validators:          - operator: ">="            expected_value: 3.2          - operator: "<="            expected_value: 3.4      - name: Inrush Current        unit: mA        validators:          - operator: "<="            expected_value: 450

Run it with tofupilot run ./procedure.yaml.

Two structural points that make this ATE rather than a script:

Safety gates power. depends_on: [safety] means the supply is never enabled on an open fixture. That is a hard requirement of any ATE that energises anything.

Power-down lives in the plug. close() runs at teardown regardless of how the run ended, so a failure does not leave a board energised on the bench.

Contact Resistance Is the Most Common ATE Failure

Pogo pins wear, oxidise and collect flux. A worn fixture produces intermittent failures on measurements that have nothing to do with the board, and the usual response is to blame the product.

The defence is to measure the interface itself rather than inferring it:

phases/contact_check.py
def contact_check(measurements, dmm):    """Four-wire resistance across a known short on the fixture."""    measurements.contact_resistance = dmm.four_wire_resistance()
procedure.yaml
- name: Contact Check    python: phases.contact_check    depends_on: [safety]    measurements:      - name: Contact Resistance        unit: "mΩ"        description: Across the fixture's calibration short        validators:          - operator: "<="            expected_value: 50

Trending that one measurement tells you when to service the fixture, before it starts failing good boards.

Scaling to Multiple Units

When the fixture holds several boards, declare slots. Each gets its own unit, measurements and outcome, and they run concurrently:

procedure.yaml
execution:  slots:    - name: Slot 1    - name: Slot 2    - name: Slot 3    - name: Slot 4

A shared resource, such as a single rack supply feeding all slots, belongs in an execution-scoped setup and teardown rather than per-slot, otherwise one slot finishing will power down its neighbours.

Recording the Station

An ATE fleet needs to know which station produced a result:

phases/record_station.py
def record_station(run):    import socket    run.metadata["station"] = socket.gethostname()    run.metadata["fixture_id"] = "FIX-4417-02"

Without it, a fixture-specific problem is indistinguishable from a product problem in the aggregate yield.

Buying vs Building

SituationDirection
Standard boards, moderate volumeBuild a bench ATE; the cost is mostly your time
High volume, tight cycle timeBuy a fixture, build the software
Component-level coverage neededBuy ICT or use flying probe
Regulated product with qualification burdenBuy, and budget for the qualification
Prototype and NPIBuild; requirements change weekly

The software layer is worth building even when you buy the hardware, because it is where the traceability and the analytics live, and it is the part that has to outlast any individual fixture.

If You Already Run OpenHTF

ate.py
27 lines
import openhtf as htffrom openhtf.plugs import BasePlugfrom openhtf.util import unitsfrom tofupilot.openhtf import uploadclass FixturePlug(BasePlug):    def lid_closed(self) -> bool:        return True@htf.measures(htf.Measurement("lid_closed").equals(True))@htf.plug(fixture=FixturePlug)def test_safety(test, fixture):    test.measurements.lid_closed = fixture.lid_closed()@htf.measures(    htf.Measurement("rail_3v3").in_range(3.2, 3.4).with_units(units.VOLT),)def test_power(test):    test.measurements.rail_3v3 = 3.31test = htf.Test(test_safety, test_power, part_number="PCBA-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan serial: "))

Install with pip install "tofupilot[openhtf]", or skip the callback and run it with tofupilot run.

Common Mistakes

MistakeConsequenceFix
No interlock gating powerFixture damage, safety incidentGate energising phases on a safety phase
Power-down as the last phaseBoard left energised after a failurePut it in the plug's close()
No contact resistance measurementFixture wear looks like product failureMeasure and trend the interface
No station or fixture IDCannot separate a bad fixture from a bad lotRecord both on every run
Recording pass/fail onlyNo drift detection, no limit reviewRecord the measured values
Shared instrument in per-slot teardownOne slot powers down its neighboursUse execution scope for shared resources

More Guides

Put this guide into practice