Hardware-in-the-Loop (HIL) Testing

Run your tests on the actual microcontroller, over a serial connection, using @typecad/expect. You write assertions in TypeScript in a testing-library style; Cuttlefish turns them into firmware, uploads it to the board, reads the results back over serial, and reports pass or fail in a single command — so you can confirm your code really works on the hardware, not just on paper.

How it works

When you run the tests, Cuttlefish turns them into firmware, uploads it, runs it, and reports back.

⚙️ Advanced details — the test pipeline

The cuttlefish-test binary runs a seven-stage pipeline:

  1. Preprocess — a transform that inspects your code’s structure rewrites your fluent assertion chains into Serial.print calls.
  2. Transpile — Cuttlefish converts the TypeScript test to C++.
  3. Compilearduino-cli compile builds the sketch.
  4. Uploadarduino-cli upload flashes the board.
  5. Capture — the host reads structured serial lines from the board.
  6. Evaluate — assertion math runs on the host (the firmware only sends raw values).
  7. Report — pass/fail output in a testing-library style.

The @typecad/expect API

import { describe, done } from '@typecad/expect';
import { A0, D13 } from '@typecad/board';

describe('Pin behavior')
  .it('reads a valid ADC value')
    .expect(A0.readAnalog()).toBeWithinRange(0, 1023)
  .it('drives the LED high')
    .expect(D13.read() ? 1 : 0).toBeTruthy();

done();
  • describe(name) opens a group and returns a Suite you chain on.
  • .it(name) starts a test case within the current group.
  • .expect(actual) asserts a numeric value (or a function returning one).
  • .expectString(actual) asserts a string value.
  • done() must be the last statement in every test file — it emits the end-of-suite sentinel.

Every matcher returns the Suite, so you chain .it(...).expect(...).matcher(...) fluently. See Expect Assertion API for every matcher.

Running tests

cuttlefish-test --port COM4
cuttlefish-test --port COM4 tests/sensor.test.ts     # one file
cuttlefish-test -p /dev/ttyACM0 -v                   # verbose

You can also run tests through the main cuttlefish CLI:

cuttlefish build --compile --upload --expect --port COM4
cuttlefish build --expect tests/sensor.test.ts --port COM4
cuttlefish build -w --compile --upload --expect --port COM4   # watch mode

Configuration

Add a test section to your cuttlefish.config.ts:

import type { CuttlefishConfig } from '@typecad/cuttlefish/api';

const config: CuttlefishConfig = {
  entry: './src/sketch.ts',
  target: 'avr',
  board: '@typecad/board-arduino-uno',
  framework: '@typecad/framework-arduino',
  frameworkData: { buildTarget: 'arduino:avr:uno' },
  test: {
    include: ['tests/**/*.test.ts'],
    port: 'COM4',
    baudRate: 115200,
    timeout: 30000,
  },
};

export default config;

CLI flags override config values. See Expect Assertion API for the full test config schema and all CLI flags.

Per-target skip directives

Some tests only apply to certain architectures. Add a file-level comment:

// @typecad-skip-target esp32: ESP32 does not expose the AVR watchdog API.

or:

// @typecad-only-target avr,megaavr: uses AVR watchdog registers.

Targets are matched against your target, the FQBN (the board ID Arduino uses) parts in frameworkData.buildTarget (e.g. esp32 in esp32:esp32:esp32), the full FQBN, and the board package name. Skipped files are reported in the output; run with --verbose to see the reason.

Full example

import { describe, done } from '@typecad/expect';
import { A0, D2, D13, I2C0 } from '@typecad/board';
import { millis } from '@typecad/hal';

describe('Sensor suite')
  .it('reads temperature within range')
    .expect(A0.readAnalog()).toBeWithinRange(0, 1023)
  .it('responds within 50ms')
    .expect(millis()).toBeGreaterThan(0)
  .it('I2C bus is addressable')
    .expect(I2C0.scan().length).toBeGreaterThanOrEqual(0);

done();

Run cuttlefish-test --port COM4. The output is vitest-style:

 ✓ tests/sensor.test.ts (3 passed)
 Tests  3 passed (3)
 PASS   All tests passed

For the complete matcher reference and config details, see Expect Assertion API. For mapping compile errors back to TypeScript, see Source-Mapped Diagnostics.