Test Types & Methods

What Is End-of-Line Testing

End-of-line testing is the last gate before a product ships. What it covers, how it differs from FCT, and how to structure it for cycle time.

JJulien Buteau
beginner8 min readMarch 14, 2026

End-of-line (EOL) test is the last verification a product receives before it is packed. Everything upstream tested parts of it; EOL tests the thing the customer will actually receive, fully assembled, in its enclosure, behaving as a product rather than as a board.

What EOL Covers

AreaTypical checks
Power-on behaviourBoots, correct current draw, no fault indications
FunctionalEvery feature the datasheet claims, exercised
SafetyHipot, ground bond, leakage where applicable
CalibrationFinal coefficients measured and written
InterfacesConnectors, buttons, displays, LEDs, audio
WirelessRF power, sensitivity, pairing, antenna presence
SensorsReadings against a reference, in the final enclosure
IdentitySerial number, MAC, firmware version, labels
CosmeticScratches, gaps, label placement

The enclosure matters more than it looks. A board that passed FCT can fail EOL because an antenna is detuned by the case, a sensor reads differently once enclosed, or a connector does not align with its cutout. Those are precisely the defects EOL exists to find.

EOL vs FCT

FCTEOL
UnitBare PCBAFully assembled product
TestsBoard-level functionProduct-level function
CatchesAssembly and component faultsIntegration, mechanical, enclosure faults
AccessTest points, pogo fixtureProduct connectors only
Cycle timeSeconds to a minuteOften minutes
ReworkCheap; board is accessibleExpensive; disassembly required

The access difference drives the design. At EOL you generally cannot probe a test point, so the test talks to the product the way a customer would: over USB, the console, or the radio. This is why firmware self-test hooks matter so much at this stage.

Cycle Time Is the Constraint

EOL usually sits at the end of a line with a takt time, and a test that overruns it becomes the bottleneck for the whole factory. Two structural levers matter.

Run independent checks concurrently. Most EOL checks do not depend on each other. Declaring them as independent phases lets them run in parallel rather than in sequence.

Test several units at once. When the fixture holds more than one product, fixture slots test them concurrently, each producing its own run and outcome.

procedure.yaml
92 lines
name: Product EOL Testversion: 1.0.0description: End-of-line test for the assembled productunit:  serial_number:    default_value: "PRD000001"  part_number:    default_value: "PRD-100"plugs:  - name: fixture    python: plugs.fixture:Fixture  - name: dut    python: plugs.dut:Dut  - name: dmm    python: plugs.dmm:Multimetermain:  - name: Safety    python: phases.safety    measurements:      - name: Enclosure Closed        validators:          - operator: "=="            expected_value: true  - name: Power On    python: phases.power_on    depends_on: [safety]    measurements:      - name: Inrush Current        unit: A        validators:          - operator: "<="            expected_value: 1.5      - name: Boot Time        unit: ms        validators:          - operator: "<="            expected_value: 3000  - name: Identity    python: phases.identity    depends_on: [power_on]    measurements:      - name: Firmware Version        validators:          - operator: "in"            expected_value: ["2.1.0", "2.1.1"]      - name: Serial Format        validators:          - operator: "matches"            expected_value: "^PRD[0-9]{6}$"  - name: Sensors    python: phases.sensors    depends_on: [power_on]    measurements:      - name: Temperature Reading        unit: "°C"        description: Against the fixture reference, in the enclosure        validators:          - operator: ">="            expected_value: 20.0          - operator: "<="            expected_value: 30.0  - name: Interfaces    python: phases.interfaces    depends_on: [power_on]    measurements:      - name: Button Response        validators:          - operator: "=="            expected_value: true      - name: LED Check        validators:          - operator: "=="            expected_value: true  - name: Wireless    python: phases.wireless    depends_on: [power_on]    measurements:      - name: TX Power        unit: dBm        validators:          - operator: ">="            expected_value: 8.0          - operator: "<="            expected_value: 14.0

Identity, sensors, interfaces and wireless all depend on power-on but not on each other, so the four run concurrently and the phase group costs the longest of them rather than their sum.

phases/power_on.py
def power_on(measurements, fixture, dut, dmm):    fixture.power_on()    measurements.inrush_current = dmm.peak_current()    measurements.boot_time = dut.wait_for_boot_ms()
phases/identity.py
def identity(measurements, dut):    measurements.firmware_version = dut.firmware_version()    measurements.serial_format = dut.serial_number()
phases/sensors.py
def sensors(measurements, dut):    measurements.temperature_reading = dut.read_temperature()

Run it with tofupilot run ./procedure.yaml.

For a multi-up fixture:

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

A failure in one position stops that position and leaves the others testing.

Firmware Self-Test Carries EOL

Because probe access is gone, the product has to report on itself. A structured self-test command is the highest-value EOL feature a firmware team can provide:

plugs/dut.py
import jsonclass Dut:    def selftest(self) -> dict:        raw = self._query("selftest --json")        return json.loads(raw)    def _query(self, cmd: str) -> str:        return '{"flash_ok": true, "imu_ok": true, "adc_counts": 2048}'
phases/selftest.py
def selftest(measurements, dut):    result = dut.selftest()    measurements.flash_ok = result["flash_ok"]    measurements.imu_ok = result["imu_ok"]    measurements.adc_counts = result["adc_counts"]

JSON rather than prose, so a firmware wording change does not break production.

Calibration at EOL

Final calibration usually belongs here, because the sensor is in its final mechanical and thermal environment. Record both the raw measurement and the coefficient written:

phases/calibrate.py
def calibrate(measurements, dut, fixture):    reference = fixture.reference_temperature()    raw = dut.read_raw_temperature()    offset = reference - raw    measurements.reference_temperature = reference    measurements.raw_temperature = raw    measurements.temperature_offset = offset    dut.write_calibration(offset)

Storing the coefficient per serial means a field complaint can be checked against what was actually programmed into that unit.

Labels and Identity

EOL is where the product gets its identity, and where mismatches are cheapest to catch. Verify that what is printed matches what is programmed:

phases/label_check.py
def label_check(measurements, dut, scanner):    programmed = dut.serial_number()    printed = scanner.read_label()    measurements.label_matches = programmed == printed

A unit whose label and firmware disagree is untraceable for its entire life. This check costs one scan.

What EOL Cannot Do

EOL is a short test on a finished product. It does not find defects that take time to appear, which is what burn-in and ongoing reliability testing are for. A product can pass EOL and fail at 400 hours, and no amount of EOL coverage changes that.

It also cannot diagnose at component level. An EOL failure usually means disassembly and a trip back to a board-level test, which is why upstream coverage still matters.

If You Already Run OpenHTF

eol.py
19 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures(    htf.Measurement("boot_time_ms")    .in_range(maximum=3000)    .with_units(units.MILLISECOND),    htf.Measurement("firmware_version").equals("2.1.0"),)def phase_power_on(test):    test.measurements.boot_time_ms = 1850    test.measurements.firmware_version = "2.1.0"test = htf.Test(phase_power_on, part_number="PRD-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan product serial: "))

Install with pip install "tofupilot[openhtf]", or run it with tofupilot run.

Common Mistakes

MistakeConsequenceFix
Sequential phases on a takt-time lineEOL becomes the bottleneckDeclare independent phases; use slots
Assuming FCT coverage is enoughEnclosure and integration faults shipTest the assembled product as a product
Firmware self-test printing proseProduction breaks on a wording changeStructured JSON output
Not verifying label against firmwareUntraceable units in the fieldScan and compare
Calibration before final assemblyCoefficients wrong in the real environmentCalibrate at EOL
Treating EOL as a reliability testTime-dependent failures still escapeAdd burn-in or ORT

More Guides

Put this guide into practice