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:
| Option | Type | Effect |
|---|---|---|
echo | boolean | Send writes back as reads (round-trip testing). |
record | boolean | Keep every byte written so you can inspect with port.binding.recording. |
readyData | Buffer | Push initial bytes after open — fakes a boot banner. |
maxReadSize | number | Cap each read chunk (default 1024). Useful for backpressure tests. |
manufacturer / vendorId / productId | string | What list() reports for this port. |
echoDelay | number | Milliseconds 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
/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.