Testing guide
August 11, 2026 ยท View on GitHub
Tests in this repository are organized by the contract they exercise, not by the implementation function they happen to call.
Test boundaries
- Unit tests exercise validation, selection, parsing, and state transitions without device I/O.
- Wire tests assert the exact CTAP or CTAPHID command, encoded field presence and omission, decoded values, status codes, and typed errors.
- Integration tests exercise several layers together through a package-local
fake transport or device. Tests requiring physical hardware stay outside the
automated suite and are documented in
README.md.
Keep arrange, act, and assert phases visually distinct. Use require for
preconditions whose failure makes the rest of a test unsafe or meaningless,
and assert for independent observations of the result.
Tables and names
Use a table only when every case describes the same contract and follows the
same setup and assertion path. Case names should describe the condition and
expected outcome, for example missing token/rejected before I/O. Do not put
unrelated commands, protocol oracles, malformed-wire cases, and concurrency
sequences into one table merely because their Go shape is similar.
Every migrated scenario must retain its inputs and observable expectations.
In particular, preserve exact wire fields, errors.Is and errors.As checks,
cache or close state, and assertions that validation performs no I/O.
Helpers and fixtures
Helpers are package-local, narrow, and live in test_helpers_test.go when they
are shared by multiple test files. A helper must:
- accept
testing.TBwhere practical and callHelper()immediately; - express protocol setup or an assertion rather than reproduce production control flow;
- return fresh mutable slices and maps on every call;
- avoid package-level mutable state and hidden ordering dependencies.
Keep stateful cryptographic, blocking, multiplexing, and cancellation fakes beside the tests whose behavior they model. Prefer several small domain fakes over a universal mock with switches for unrelated protocols.
Concurrency
No test may wait indefinitely on a channel, goroutine, or device operation.
Use a bounded receive helper with a useful timeout diagnostic and register
cleanup for goroutines and devices at construction time. Avoid t.Parallel()
when a test owns goroutines or shared fake state. Assertions should verify
close and cancellation ordering directly instead of relying on sleeps.
Before submitting test changes, run the focused package tests, then:
gofmt -l .
go vet ./...
go test ./...
For changes to transport concurrency, also run the race/stress command documented in the relevant change plan.
Opt-in BLE hardware test
The BLE hardware test is excluded from normal runs. On macOS, grant Bluetooth permission to the terminal and make the FIDO authenticator available for pairing, then run:
CTAP_BLE_TEST=1 go test ./backend/ble -run TestHardwareGetInfo -v
The transport relies on macOS to establish an encrypted link when it accesses the protected FIDO GATT characteristics. The first run may therefore display a system pairing prompt; an existing bond is reused without another prompt.
If more than one FIDO BLE authenticator is advertising, select one by its opaque CoreBluetooth identifier:
CTAP_BLE_TEST=1 CTAP_BLE_DEVICE_ID=01234567-89AB-CDEF-0123-456789ABCDEF \
go test ./backend/ble -run TestHardwareGetInfo -v