Your rust embedded unit test runs on the host. The Book barely mentions it.

cargo test in an embedded Rust project either refuses to compile or passes
suspiciously fast. Both outcomes confuse people, and both have the same cause:
nobody said which machine the test was going to run on.
Zephyr has native_sim and unit_testing: two host binaries, two different machines, and a docs page that had to print the words "fundamentally different" because engineers kept treating them as two spellings of the same thing.
Rust has the same split. It has far fewer words written about it.
Plain cargo test on a host target is the unit_testing-shaped thing: a native
x86 or ARM64 process, std present, the libtest harness, no board, no probe.
cargo build --target thumbv7em-none-eabihf with no test run is a different,
middle thing: it proves the code compiles for the architecture, nothing about
runtime behavior. With a device-side harness and probe-rs run configured as
the target runner, cargo test can flash and execute the test binary on
silicon. Three checks, three different kinds of evidence.
What the Book says about this
Close to nothing.
The Embedded Rust Book has no unit-testing chapter. The nearest thing is a single bullet in the Portability chapter, listing the flavours a HAL implementation can come in: low-level hardware access via registers, an operating system such as sysfs under Linux, "via adapter, e.g. a mock of types for unit testing", and via a driver for hardware adapters like an I2C multiplexer.1
That is the official treatment. A mock-adapter pattern, mentioned in passing, inside a chapter about something else.
I am not going to inflate that into "the Book already explains the host/target
split." It does not. It gestures at swapping the hardware layer
for something host-friendly, the same idea the C world argues about every year in
CMock is not a simulator, and stops. It
does not tell you that cargo test will fail to build, why it fails, or what to
type instead.
Someone asked for precisely that. A 2023 issue on rust-embedded/discovery, the
Book's companion tutorial project, reads: "its gotten to the point where I need
to unit test some of my non-hardware dependent functionality... How should I set
up unit tests for testing individual modules in embedded projects? Examples would
be massively appreciated."2
The gap is known. It is still a gap. So the explaining happened elsewhere: on Ferrous Systems' blog, in Embassy's CI scripts, and in forum threads where people re-derive the answer one at a time. That pattern is the same shape as the Zephyr confusion: thin official docs, community filling in by hand, everyone arriving surprised. It is most of why this post exists.

Why cargo test fails at all
The mechanism is boring, which is why it catches people.
cargo test links libtest. libtest depends on std. The defmt-test README says
it in one line: the default test harness, the test crate, depends on std.3
Your project almost certainly has a .cargo/config.toml with something like
[build] target = "thumbv7em-none-eabihf". Cargo applies that default target to
every subcommand, not just build. So cargo test tries to compile a
std-dependent harness for a bare-metal triple, and you get
error[E0463]: can't find crate for 'test'.
A 2019 internals thread has the cleanest statement of the problem I have found: "Rust's test library requires the standard library so we can't use it in no_std environments. But what we can do is to run platform independent unit tests on the host system... The problem is that cargo uses the specified default target for all subcommands and tries to compile the std-dependent tests for our bare metal system, which of course fails."4
People hit this and reach for an on-target harness, because the error mentions
the target and they assume the target is required. Usually it is not. A user in
2020 worked it out mid-thread: "Ah yes — I see now that I've got a .cargo/config
file that declares a default build target. Performing cargo test --target x86_64-apple-darwin does indeed work."5 A 2023 answer states the rule better
than any doc page: for a lib crate, if your tests can run on the host, meaning
you are not testing hardware-specific functionality, the default libtest harness
works fine, and you only need defmt-test when the test has to execute on the
target architecture.6
Ferrous Systems drew the same line back in 2021: functional, IO-less code gets
tested on the host with the built-in #[test] machinery, and code that interacts
with hardware gets tested on the target with defmt-test.7 That is the whole
taxonomy. It lives on a consultancy blog, not in the Book.
The mechanical fix is one attribute. #![cfg_attr(not(test), no_std)] makes the
crate no_std for normal builds and lets std back in under cfg(test), so the
same source compiles two ways. Combine it with an explicit --target <host triple>, or split the register-touching code into its own crate away from the
logic: PAC access, cortex-m intrinsics, interrupt vectors.7

Embassy already sorted this out in public
The best documentation of the Rust host/target split is not prose. It is a shell script.
Embassy's .github/ci/test.sh runs plain cargo test on the host, and not only
against pure-logic crates:
cargo test --manifest-path ./embassy-executor/Cargo.toml --features metadata-name
cargo test --manifest-path ./embassy-sync/Cargo.toml
cargo test --manifest-path ./embassy-time/Cargo.toml --features mock-driver,embassy-time-queue-utils/generic-queue-8
cargo test --manifest-path ./embassy-nrf/Cargo.toml --no-default-features --features nrf52840,time-driver-rtc1,gpiote
cargo test --manifest-path ./embassy-stm32/Cargo.toml --no-default-features --features stm32f429vg,time-driver-any,exti,single-bank,low-power,chrono,test
Those last two lines are the interesting ones. embassy-nrf with nrf52840 and
embassy-stm32 with stm32f429vg are chip HAL crates, compiled and tested on a
CI runner with no nRF and no STM32 attached.8 The trick is the first line of
embassy-stm32/src/lib.rs: #![cfg_attr(not(test), no_std)].9
An Embassy maintainer spelled out the recipe to a user who filed an issue titled
"Any guidance on the correct way to allow cargo test to work?": make the crate
no_std only when not testing with #![cfg_attr(not(test), no_std)], accept that
this builds embedded-only libs for x86 and that this might or might not work, and
adjust dependency features: embassy-nrf needs default-features = false to
disable rt, because that is how it is tested in CI.10
"Might or might not work" is the honest part. Host-testing a HAL crate gets you the type system, the state machines, the arithmetic. It does not get you the peripheral.
Which is why Embassy keeps the other half somewhere else: tests/stm32/,
tests/nrf/, tests/rp/, flashed onto a hardware farm by teleprobe, Embassy's
own wrapper that uses probe-rs underneath for flashing, attaching and decoding
RTT.11 A review comment on one PR makes the division sound as ordinary as it
should: can you add on-hardware tests for HASH here, this is a prime candidate for
testing since it needs no connections to external hardware.11
Two directories. Two judges. Nobody in that repo confuses them.

The target side, precisely
There is no probe-rs test. probe-rs has run, attach, and a handful of
other device commands, but no subcommand dedicated to running tests.12
You point Cargo at probe-rs run as the target runner, so cargo test hands
it a binary the way it would hand one to any runner:
[target.thumbv7em-none-eabihf]
runner = 'probe-rs run --chip STM32F767ZITx'
probe-rs run flashes, resets, starts the binary and streams RTT and defmt output
plus panics back to your console, for any embedded binary in any language.12
probe-rs attach is the same thing without the reset and without flashing. The
maintainers' current guidance is short: prefer probe-rs run for new setups,
cargo-embed adds an interactive RTT terminal but is expected to be phased out,
and cargo-flash just flashes.13 All of it needs a physical debug probe
translating host USB or Ethernet into SWD or JTAG.13
defmt-test is the Knurling harness for the device side: a test harness that lets
you write and run unit tests on your device as if you were using the built-in
#[test] attribute. You put #[defmt_test::tests] on a mod, write #[test]
functions inside, and you get #[init], #[before_each], #[after_each],
#[teardown] and #[should_error]. You must set harness = false in
Cargo.toml, because the default harness needs std.3
embedded-test, from the probe-rs org, makes the host half explicit instead of
implicit. It is a libtest-compatible runner where, for each test case, probe-rs
resets the device, signals which test to run via semihosting SYS_GET_CMDLINE,
and waits for the device to report success or failure via SYS_EXIT. probe-rs run autodetects whether the ELF is normal firmware or a test binary.14
And it is not theoretical. rp-hal's migration off the deprecated probe-run shows
cargo test reaching silicon:15
$ CARGO_TARGET_THUMBV6M_NONE_EABI_RUNNER="probe-rs run" cargo test --test dma_m2m_u8 -- --chip rp2040
Finished test [optimized + debuginfo] target(s) in 0.03s
Running tests/dma_m2m_u8.rs (target/thumbv6m-none-eabi/debug/deps/dma_m2m_u8-ebcd141c1ecc1d99)
Erasing sectors ✓ ...
"Erasing sectors" is the line that tells you which category you are in.
What each pass buys you
A green host cargo test says your logic is correct on x86, your state machines
step the way you asserted, your parsers parse, your arithmetic does not overflow,
and, if you did the Embassy thing, large parts of a HAL crate compile and behave
for a chosen chip feature set. That is a lot, it runs in seconds, it needs no
board, and you get the whole host toolchain: coverage, sanitizers, a real
debugger.
It says nothing about a register write, a peripheral, an interrupt latency, or the image you are about to ship. Not because host testing is weak, but because that binary was never built for your target.
A green probe-rs run pass says the flashed image did the thing on that specific
chip, once, on the desk it was attached to.
Both belong in CI. Most pipelines have neither: firmware CI ends at the linker for a depressing share of teams, and "it compiles for thumbv7em" is not a test result.
The short version: pure logic goes to host cargo test. Target compatibility
goes to a target-triple build with no run. Peripheral or interrupt behavior
goes to an on-device harness through probe-rs run.
The one thing to stop doing is what the Zephyr crowd does with native_sim:
running the fast one and reporting it as the slow one. Rust makes that easier to
do by accident, because both invocations start with cargo test.
Zephyr eventually wrote "fundamentally different" into its docs. The Embedded Rust Book has a bullet about mock adapters. The community filled the rest in by hand, one forum thread at a time, and that is worth naming rather than pretending the docs cover it.
Sources
Footnotes
-
The Embedded Rust Book, "Portability," §HAL implementation, accessed 2026-08-21. https://docs.rust-embedded.org/book/portability/index.html ↩
-
rust-embedded, "Unit testing?," discovery issue 544, 2023-10-28. https://github.com/rust-embedded/discovery/issues/544 ↩
-
Knurling / Ferrous Systems, "defmt-test," README, accessed 2026-08-21. https://github.com/knurling-rs/defmt/blob/main/firmware/defmt-test/README.md ↩ ↩2
-
internals.rust-lang.org, "Set default target for cargo build but not for cargo test," 2019-04-06. https://internals.rust-lang.org/t/set-default-target-for-cargo-build-but-not-for-cargo-test/9777/1 ↩
-
users.rust-lang.org, "Running doc and unit tests for nostd," 2020-10-16. https://users.rust-lang.org/t/running-doc-and-unit-tests-for-nostd/50179 ↩
-
users.rust-lang.org, "Cargo test for embedded target fails," 2023-08-25. https://users.rust-lang.org/t/cargo-test-for-embedded-target-fails/98926 ↩
-
Ferrous Systems, "Testing an embedded application," 2021. https://ferrous-systems.com/blog/test-embedded-app/ ↩ ↩2
-
Embassy, host-side CI script
.github/ci/test.sh, commit 63e74af. https://github.com/embassy-rs/embassy/blob/63e74af9badbb4e82580e3121c4b341dcf63d5cc/.github/ci/test.sh ↩ -
Embassy,
embassy-stm32/src/lib.rsline 1, commit 63e74af. https://github.com/embassy-rs/embassy/blob/63e74af9badbb4e82580e3121c4b341dcf63d5cc/embassy-stm32/src/lib.rs#L1 ↩ -
Embassy, "Any guidance on the correct way to allow cargo test to work?," issue 1564, 2023-06-16. https://github.com/embassy-rs/embassy/issues/1564 ↩
-
Embassy, pull request 2528, and the
teleproberepository. https://github.com/embassy-rs/embassy/pull/2528 and https://github.com/embassy-rs/teleprobe ↩ ↩2 -
probe-rs, "probe-rs CLI," documentation, accessed 2026-08-21. https://probe.rs/docs/tools/probe-rs/ ↩ ↩2
-
probe-rs, crates.io README, and "About probe-rs." https://crates.io/crates/probe-rs and https://probe.rs/docs/overview/about-probe-rs/ ↩ ↩2
-
probe-rs, "embedded-test" v0.7.1 documentation. https://docs.rs/embedded-test/latest/embedded_test/ and https://github.com/probe-rs/embedded-test/ ↩
-
rp-hal, "Migrate from probe-run to probe-rs run," pull request 698, and Ferrous Systems, "probe-run deprecation." https://github.com/rp-rs/rp-hal/pull/698 and https://ferrous-systems.com/blog/probe-run-deprecation/ ↩