SerialPilot

/02 — Reference

Testing & mocking

Hardware lies. Hardware is also slow, expensive, and not in CI. @serialpilot/binding-mock gives you a virtual port that behaves like the real one, so the same code paths — parsers, error handlers, reconnect logic — run in unit tests too.

Why mock instead of stub

Stubbing the SerialPilot class itself is brittle: every change to its API ripples into every test. The mock binding sits one layer lower, where the surface is small and stable. Your code keeps using SerialPilot normally; only the byte-pump underneath swaps.

Use SerialPilotMock

The simplest path is the pre-wired class:

import { SerialPilotMock, ReadlineParser } from 'serialpilot'
import { MockBinding } from '@serialpilot/binding-mock'

MockBinding.createPort(‘/dev/ROBOT’, { echo: true })

const port = new SerialPilotMock({ path: ‘/dev/ROBOT’, baudRate: 9600 }) const lines = port.pipe(new ReadlineParser({ delimiter: ‘\n’ })) lines.on(‘data’, line => expect(line).toBe(‘hello’))

port.write(‘hello\n’) // echo: lines emits ‘hello’

SerialPilotMock is a SerialPilot with the mock binding bolted on at construction time — every other method works identically.

MockBinding.createPort(path, options)

Create a virtual port. Options:

OptionTypeEffect
echobooleanSend writes back as reads (round-trip testing).
recordbooleanKeep every byte written so you can inspect with port.binding.recording.
readyDataBufferPush initial bytes after open — fakes a boot banner.
maxReadSizenumberCap each read chunk (default 1024). Useful for backpressure tests.
manufacturer / vendorId / productIdstringWhat list() reports for this port.
echoDelaynumberMilliseconds before the echo arrives. Simulates baud-rate latency.
disconnectAfter{ bytesWritten: number }Trigger a disconnect after a write threshold — exercise reconnect code.
respondTo{ [pattern]: Buffer | (data) => Buffer }Programmable replies — match input, return output. Stub a whole protocol.
periodicData{ data: Buffer, intervalMs: number }Stream bytes at a fixed cadence. Sensor simulators.

Patterns

Stub a request/response protocol

MockBinding.createPort('/dev/MODEM', {
respondTo: {
'AT\r\n':        Buffer.from('OK\r\n'),
'AT+CSQ\r\n':    Buffer.from('+CSQ: 21,0\r\nOK\r\n'),
},
})

Force a disconnect mid-stream

MockBinding.createPort('/dev/FLAKY', {
echo: true,
disconnectAfter: { bytesWritten: 256 },
})

// after 256 bytes, the next read errors with DisconnectedError

Fake a boot banner

MockBinding.createPort('/dev/ARDUINO', {
readyData: Buffer.from('READY\r\n'),
})

const port = new SerialPilotMock({ path: ‘/dev/ARDUINO’, baudRate: 9600 }) const ready = port.pipe(new ReadyParser({ delimiter: ‘READY’ })) ready.on(‘ready’, () => /* go */)

Make ports show up in list()

MockBinding.createPort('/dev/ROBOT', { manufacturer: 'Acme Robotics' })
const ports = await MockBinding.list() // includes /dev/ROBOT

Cleaning up between tests

Call MockBinding.reset() in a beforeEach/afterEach hook — every virtual port is wiped and the serial counter resets:

beforeEach(() => MockBinding.reset())

Test error paths

Every SerialPilotError subclass works identically against the mock. Test that your code handles a port-busy or write-failed without unplugging anything:

import { CancelledError } from 'serialpilot'

const port = new SerialPilotMock({ path: ‘/dev/ROBOT’, baudRate: 9600 }) const pending = port.read() port.close(() => {}) // pending operations get CancelledError

CI reminder Tests that hit /dev/tty* or COM* will fail in CI. Anything that imports serialpilot works in CI as long as you use the mock binding for that test's port instance.

Edit this page on GitHub