Run your PlatformIO unit tests without hardware — or anything installed
Category: Tutorial
Running pio test on real hardware means a board on the desk, a USB cable, and a free hand to press reset on push. With LabWired, none of that is necessary.
This guide wires your PlatformIO pio test suite into the LabWired simulator, a deterministic digital twin of your MCU. Your tests don’t change and PlatformIO still parses the usual Unity output; the only thing missing is the board. If you take the GitHub Actions path below, you won’t install anything locally either.
It plugs into PlatformIO the same way Renode and QEMU do: through the built-in test_testing_command hook. PlatformIO doesn’t need to know anything about LabWired; it runs a command and reads the Unity results from its output.
Start in GitHub Actions (nothing to install)
Fork w1ne/labwired-core, drop this into .github/workflows/firmware-tests.yml, and push:
name: Firmware tests (LabWired simulator)
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install PlatformIO
run: pipx install platformio
- uses: dtolnay/rust-toolchain@stable
- name: Build the LabWired CLI and put it on PATH
run: |
cargo build --release -p labwired-cli
echo "$PWD/target/release" >> "$GITHUB_PATH"
- name: Run unit tests in the simulator
working-directory: examples/platformio/nrf52840-unity
run: pio test -e nrf52840_dk
GitHub’s runner installs PlatformIO, builds the firmware, runs it in the simulator, and reports per-test results, all without touching your machine:
test/test_smoke/test_main.c:50: test_addition [PASSED]
test/test_smoke/test_main.c:51: test_uart_is_enabled [PASSED]
test/test_smoke/test_main.c:52: test_string_length [PASSED]
================== 3 test cases: 3 succeeded in 00:00:01.4 ==================
If you'd rather poke at it from a terminal, open the repo in a GitHub Codespace and run cd examples/platformio/nrf52840-unity && pio test. That still keeps everything off your machine, since the Codespace runs in your browser.
How it works
flowchart LR
A["pio test<br/>builds firmware.elf"] -- ELF --> B["LabWired simulator<br/>boots the ELF · deterministic"]
B -- "UART → stdout" --> C["PlatformIO<br/>parses Unity PASS/FAIL"]
C --> D(("✓"))
classDef ok fill:#27c93f,stroke:#1a1a1a,stroke-width:2px,color:#0d2b12;
class D ok;
Caption: The board’s role is played by a process; the rest of the pipeline is unchanged.
Both sides are plain headless processes connected by a pipe. There is nothing to flash, no probe to enumerate, and no board state to drift between runs — which is where most hardware-in-the-loop flakiness comes from.
The whole integration
It’s a few lines of platformio.ini:
[platformio] ; so the ${platformio.*} vars resolve
[env:nrf52840_dk]
platform = nordicnrf52
board = nrf52840_dk
test_testing_command =
labwired test --script labwired.test.yaml
--firmware ${platformio.build_dir}/${this.__env__}/firmware.elf
pio test builds firmware.elf, then runs this command and parses its output. The only firmware-side requirement is that Unity’s output reaches the UART that LabWired mirrors to stdout. With the Arduino, Zephyr, or mbed frameworks, that is your normal test serial already.
What you get out of it
- Determinism. The same inputs produce the same results on every run, so a red build means a real regression rather than a timing fluke.
- Parallelism. CI can run every board and configuration at once; a bench full of hardware can’t.
- No hardware in the loop. Contributors and CI runners don’t need a board to run the suite.
- Observability. Every run leaves behind UART logs, VCD traces, and a machine-readable
result.json.
The simulator executes your firmware on a full Cortex-M core model with peripherals validated against real silicon, so the instructions that run in CI are the ones that would run on the board.